diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/ClaimActivity.java b/claimManagement/src/main/java/org/openimis/imisclaims/ClaimActivity.java index 92a2f948..f22ba8bc 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/ClaimActivity.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/ClaimActivity.java @@ -10,7 +10,9 @@ import android.database.Cursor; import android.inputmethodservice.Keyboard; import android.os.Bundle; +import android.text.Editable; import android.text.InputType; +import android.text.TextWatcher; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -93,7 +95,32 @@ public static Intent newIntent(@NonNull Context context, @NonNull String claimUU RadioButton rbEmergency, rbReferral, rbOther, rbPositive, rbNegative; ImageButton btnScan; LinearLayout llFagepFields; - TextInputLayout ettClaimPrefix, ettGuaranteeNo; + TextInputLayout ettClaimPrefix, ettGuaranteeNo, ettClaimCode; + private volatile ValidationResult latestValidationResult; + + private static class LocalClaimCodeValidationResult { + private final boolean valid; + private final String errorMessage; + + private LocalClaimCodeValidationResult(boolean valid, String errorMessage) { + this.valid = valid; + this.errorMessage = errorMessage; + } + } + + private static class ValidationResult { + private final boolean valid; + private final View errorView; + private final String errorMessage; + private final LocalClaimCodeValidationResult localClaimCodeValidationResult; + + private ValidationResult(boolean valid, View errorView, String errorMessage, LocalClaimCodeValidationResult localClaimCodeValidationResult) { + this.valid = valid; + this.errorView = errorView; + this.errorMessage = errorMessage; + this.localClaimCodeValidationResult = localClaimCodeValidationResult; + } + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -140,6 +167,10 @@ protected void onCreate(Bundle savedInstanceState) { etVisitType = findViewById(R.id.etVisitType); ettClaimPrefix = findViewById(R.id.ettClaimPrefix); ettGuaranteeNo = findViewById(R.id.ettGuaranteeNo); + View claimCodeParent = (View) etClaimCode.getParent(); + if (claimCodeParent instanceof TextInputLayout) { + ettClaimCode = (TextInputLayout) claimCodeParent; + } String[] visitTypes = getResources().getStringArray(R.array.visitType); ArrayAdapter visitTypeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, visitTypes); @@ -170,6 +201,25 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon rgVisitType.setVisibility(View.GONE); ettGuaranteeNo.setVisibility(View.GONE); + TextWatcher claimCodeUniquenessWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // no-op + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // no-op + } + + @Override + public void afterTextChanged(Editable s) { + validateLocalClaimCodeUniqueness(); + } + }; + etClaimCode.addTextChangedListener(claimCodeUniquenessWatcher); + etClaimPrefix.addTextChangedListener(claimCodeUniquenessWatcher); + tvItemTotal.setText("0"); tvServiceTotal.setText("0"); @@ -267,7 +317,14 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon btnPost.setOnClickListener(v -> { progressDialog = ProgressDialog.show(this, "", getResources().getString(R.string.Processing)); runOnNewThread( - () -> isValidData() && saveClaim(), + () -> { + ValidationResult validationResult = validateDataForSubmission(); + latestValidationResult = validationResult; + if (!validationResult.valid) { + return false; + } + return saveClaim(); + }, () -> runOnUiThread(() -> { ClearForm(); progressDialog.dismiss(); @@ -278,7 +335,12 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon } })); }), - () -> progressDialog.dismiss(), + () -> runOnUiThread(() -> { + if (latestValidationResult != null) { + renderValidationResult(latestValidationResult); + } + progressDialog.dismiss(); + }), 500 ); }); @@ -756,26 +818,23 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } - private boolean isValidData() { + private ValidationResult validateDataForSubmission() { + LocalClaimCodeValidationResult localClaimCodeValidationResult = checkLocalClaimCodeUniqueness(); if (etHealthFacility.getText().length() == 0) { - showValidationDialog(etHealthFacility, getResources().getString(R.string.MissingHealthFacility)); - return false; + return invalidValidationResult(etHealthFacility, getResources().getString(R.string.MissingHealthFacility), localClaimCodeValidationResult); } if (sqlHandler.getAdjustability("ClaimAdministrator").equals("M") && etClaimAdmin.getText().length() == 0) { - showValidationDialog(etClaimAdmin, getResources().getString(R.string.MissingClaimAdmin)); - return false; + return invalidValidationResult(etClaimAdmin, getResources().getString(R.string.MissingClaimAdmin), localClaimCodeValidationResult); } if (etClaimCode.getText().length() == 0) { - showValidationDialog(etClaimCode, getResources().getString(R.string.MissingClaimCode)); - return false; + return invalidValidationResult(etClaimCode, getResources().getString(R.string.MissingClaimCode), localClaimCodeValidationResult); } if (etInsureeNumber.getText().length() == 0) { - showValidationDialog(etInsureeNumber, getResources().getString(R.string.MissingCHFID)); - return false; + return invalidValidationResult(etInsureeNumber, getResources().getString(R.string.MissingCHFID), localClaimCodeValidationResult); } /*if (!etProgram.getText().toString().equals("VIH")) { @@ -787,18 +846,15 @@ private boolean isValidData() { }*/ if (!isValidInsureeNumber()) { - showValidationDialog(etInsureeNumber, getResources().getString(R.string.InvalidCHFID)); - return false; + return invalidValidationResult(etInsureeNumber, getResources().getString(R.string.InvalidCHFID), localClaimCodeValidationResult); } if (etStartDate.getText().length() == 0) { - showValidationDialog(etStartDate, getResources().getString(R.string.MissingStartDate)); - return false; + return invalidValidationResult(etStartDate, getResources().getString(R.string.MissingStartDate), localClaimCodeValidationResult); } if (etEndDate.getText().length() == 0) { - showValidationDialog(etEndDate, getResources().getString(R.string.MissingEndDate)); - return false; + return invalidValidationResult(etEndDate, getResources().getString(R.string.MissingEndDate), localClaimCodeValidationResult); } try { @@ -810,36 +866,34 @@ private boolean isValidData() { Date End_date = DateUtils.dateFromString(EndDate); if (End_date.after(Current_date)) { - showValidationDialog(etEndDate, getResources().getString(R.string.AfterCurrentDate)); - return false; + return invalidValidationResult(etEndDate, getResources().getString(R.string.AfterCurrentDate), localClaimCodeValidationResult); } if (Start_date.after(End_date)) { - showValidationDialog(etEndDate, getResources().getString(R.string.BiggerDate)); - return false; + return invalidValidationResult(etEndDate, getResources().getString(R.string.BiggerDate), localClaimCodeValidationResult); } } catch (Exception e) { Log.e(LOG_TAG, "Error while parsing dates", e); } if (etDiagnosis.getText().length() == 0) { - showValidationDialog(etDiagnosis, getResources().getString(R.string.MissingDisease)); - return false; + return invalidValidationResult(etDiagnosis, getResources().getString(R.string.MissingDisease), localClaimCodeValidationResult); } if (etProgram.getText().length() == 0) { - showValidationDialog(etProgram, getResources().getString(R.string.MissingProgram)); - return false; + return invalidValidationResult(etProgram, getResources().getString(R.string.MissingProgram), localClaimCodeValidationResult); } if(etClaimPrefix.getText().length() == 0){ - showValidationDialog(etClaimPrefix, getResources().getString(R.string.MissingChequeNumber)); - return false; + return invalidValidationResult(etClaimPrefix, getResources().getString(R.string.MissingChequeNumber), localClaimCodeValidationResult); } if(etClaimCode.getText().length() > 7){ - showValidationDialog(etClaimPrefix, getResources().getString(R.string.InvalidClaimCode)); - return false; + return invalidValidationResult(etClaimPrefix, getResources().getString(R.string.InvalidClaimCode), localClaimCodeValidationResult); + } + + if (!localClaimCodeValidationResult.valid) { + return invalidValidationResult(etClaimCode, localClaimCodeValidationResult.errorMessage, localClaimCodeValidationResult); } // if (rgVisitType.getCheckedRadioButtonId() == -1) { @@ -848,26 +902,22 @@ private boolean isValidData() { // } if(etVisitType.getText().toString().isEmpty()){ - showValidationDialog(rgVisitType, getResources().getString(R.string.MissingVisitType)); - return false; + return invalidValidationResult(rgVisitType, getResources().getString(R.string.MissingVisitType), localClaimCodeValidationResult); } if (Float.parseFloat(tvItemTotal.getText().toString()) + Float.parseFloat(tvServiceTotal.getText().toString()) == 0) { - showValidationDialog(tvItemTotal, getResources().getString(R.string.MissingClaim)); - return false; + return invalidValidationResult(tvItemTotal, getResources().getString(R.string.MissingClaim), localClaimCodeValidationResult); } if(prefixProgramCode.equals("PAL")){ if(etTestNumber.getText().length() == 0){ - showValidationDialog(etClaimPrefix, getResources().getString(R.string.MissingTestNumber)); - return false; + return invalidValidationResult(etClaimPrefix, getResources().getString(R.string.MissingTestNumber), localClaimCodeValidationResult); } if(rgTdr.getCheckedRadioButtonId() == -1){ - showValidationDialog(etClaimPrefix, getResources().getString(R.string.MissingTdr)); - return false; + return invalidValidationResult(etClaimPrefix, getResources().getString(R.string.MissingTdr), localClaimCodeValidationResult); } } - return true; + return new ValidationResult(true, null, null, localClaimCodeValidationResult); } private boolean isValidInsureeNumber() { @@ -970,4 +1020,62 @@ private boolean saveClaim() { return true; } + private LocalClaimCodeValidationResult checkLocalClaimCodeUniqueness() { + if (getIntent().hasExtra(EXTRA_CLAIM_UUID)) { + return new LocalClaimCodeValidationResult(true, null); + } + String finalCode = etClaimPrefix.getText().toString() + etClaimCode.getText().toString(); + if (finalCode.trim().isEmpty()) { + return new LocalClaimCodeValidationResult(true, null); + } + + boolean exists = sqlHandler.existsClaimCode(finalCode); + if (exists) { + String errorMessage = getResources().getString(R.string.ClaimNumberExist); + return new LocalClaimCodeValidationResult(false, errorMessage); + } + return new LocalClaimCodeValidationResult(true, null); + } + + private void renderLocalClaimCodeError(@NonNull LocalClaimCodeValidationResult result) { + if (ettClaimCode != null) { + if (result.valid) { + ettClaimCode.setError(null); + ettClaimCode.setErrorEnabled(false); + } else { + ettClaimCode.setErrorEnabled(true); + ettClaimCode.setError(result.errorMessage); + } + return; + } + if (result.valid) { + etClaimCode.setError(null); + } else { + etClaimCode.setError(result.errorMessage); + } + } + + private boolean validateLocalClaimCodeUniqueness() { + LocalClaimCodeValidationResult result = checkLocalClaimCodeUniqueness(); + renderLocalClaimCodeError(result); + return result.valid; + } + + private ValidationResult invalidValidationResult(@NonNull View errorView, @NonNull String errorMessage, @NonNull LocalClaimCodeValidationResult localClaimCodeValidationResult) { + return new ValidationResult(false, errorView, errorMessage, localClaimCodeValidationResult); + } + + private void renderValidationResult(@NonNull ValidationResult result) { + renderLocalClaimCodeError(result.localClaimCodeValidationResult); + if (result.valid) { + return; + } + showDialog(result.errorMessage, (dialog, which) -> { + if (result.errorView instanceof EditText) { + EditText editText = (EditText) result.errorView; + editText.requestFocus(); + } + }); + } + } diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/ImisActivity.java b/claimManagement/src/main/java/org/openimis/imisclaims/ImisActivity.java index a2e59d90..bd6a28b8 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/ImisActivity.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/ImisActivity.java @@ -344,6 +344,8 @@ protected void runOnNewThread(@NonNull Supplier task, @NonNull Runnable } } + // Callbacks are executed on this worker thread. + // Any UI updates inside callbacks must be wrapped in runOnUiThread(...). if (result) { onTaskSucceed.run(); } else { diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/SQLHandler.java b/claimManagement/src/main/java/org/openimis/imisclaims/SQLHandler.java index 2878c8ce..9719460d 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/SQLHandler.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/SQLHandler.java @@ -1194,6 +1194,18 @@ public String getClaimUUIDForCode(@NonNull String claimCode) { return null; } + public boolean existsClaimCode(@NonNull String claimCode) { + try (Cursor cursor = db.rawQuery( + "SELECT 1 FROM tblClaimDetails WHERE LOWER(ClaimCode) = LOWER(?) LIMIT 1", + new String[]{claimCode} + )) { + return cursor != null && cursor.moveToFirst(); + } catch (Exception e) { + Log.e(LOG_TAG, "Error while checking claim code uniqueness", e); + return false; + } + } + @NonNull public JSONObject getClaimCounts() { JSONArray claimCounts = getQueryResultAsJsonArray( diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/SynchronizeService.java b/claimManagement/src/main/java/org/openimis/imisclaims/SynchronizeService.java index ae42f97f..73a27265 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/SynchronizeService.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/SynchronizeService.java @@ -40,12 +40,15 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.UUID; import io.sentry.Sentry; +import io.sentry.SentryLevel; public class SynchronizeService extends JobIntentService { private static final int JOB_ID = 6541259; //Random unique Job id private static final String LOG_TAG = "SYNCSERVICE"; + private static final String TRACE_TAG = "SYNC_TRACE"; private static final String ACTION_UPLOAD_CLAIMS = "SynchronizeService.ACTION_UPLOAD_CLAIMS"; private static final String ACTION_EXPORT_CLAIMS = "SynchronizeService.ACTION_EXPORT_CLAIMS"; @@ -112,13 +115,37 @@ protected void onHandleWork(@NonNull Intent intent) { } private void handleUploadClaims() { + String syncSessionId = UUID.randomUUID().toString(); + long uploadStartTs = System.currentTimeMillis(); + String uploadThread = Thread.currentThread().getName() + ":" + Thread.currentThread().getId(); if (!global.isNetworkAvailable()) { + String message = String.format( + Locale.US, + "SYNC_TRACE session=%s event=UPLOAD_ABORT_NO_NETWORK ts=%d thread=%s", + syncSessionId, uploadStartTs, uploadThread + ); + Log.i(TRACE_TAG, message); + Sentry.captureMessage(message, SentryLevel.INFO); broadcastError(getResources().getString(R.string.CheckInternet), ACTION_UPLOAD_CLAIMS); return; } JSONArray claimsArray = sqlHandler.getAllPendingClaims(); + String message = String.format( + Locale.US, + "SYNC_TRACE session=%s event=UPLOAD_START ts=%d thread=%s pendingClaims=%d", + syncSessionId, uploadStartTs, uploadThread, claimsArray.length() + ); + Log.i(TRACE_TAG, message); + Sentry.captureMessage(message, SentryLevel.INFO); if (claimsArray.length() < 1) { + message = String.format( + Locale.US, + "SYNC_TRACE session=%s event=UPLOAD_ABORT_NO_CLAIM ts=%d thread=%s", + syncSessionId, System.currentTimeMillis(), uploadThread + ); + Log.i(TRACE_TAG, message); + Sentry.captureMessage(message, SentryLevel.INFO); broadcastError(getResources().getString(R.string.NoClaim), ACTION_UPLOAD_CLAIMS); return; } @@ -165,13 +192,48 @@ private void handleUploadClaims() { } else { //upload claim try { + String claimUUID = sqlHandler.getClaimUUIDForCode(claim.getClaimNumber()); + long createStartTs = System.currentTimeMillis(); + String createMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_START ts=%d thread=%s claimCode=%s claimUUID=%s insureeId=%d programId=%d", + syncSessionId, createStartTs, uploadThread, claim.getClaimNumber(), claimUUID, insureeId, programId + ); + Log.i(TRACE_TAG, createMessage); + Sentry.captureMessage(createMessage, SentryLevel.INFO); + Integer status = new CreateClaim().execute(claim, Integer.parseInt(adminId),Integer.parseInt(hfId),insureeId,programId, diagnosisId, programCode); + String createEndMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_END ts=%d thread=%s claimCode=%s claimUUID=%s status=%s durationMs=%d", + syncSessionId, System.currentTimeMillis(), uploadThread, claim.getClaimNumber(), claimUUID, + status == STATUS_ERROR ? "ERROR" : "SUCCESS", + System.currentTimeMillis() - createStartTs + ); + Log.i(TRACE_TAG, createEndMessage); + Sentry.captureMessage(createEndMessage, SentryLevel.INFO); + String createFinalMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_END ts=%d thread=%s claimCode=%s claimUUID=%s status=%s durationMs=%d", + syncSessionId, System.currentTimeMillis(), uploadThread, claim.getClaimNumber(), claimUUID, + status == STATUS_ERROR ? "ERROR" : "SUCCESS", + System.currentTimeMillis() - createStartTs + ); + Log.i(TRACE_TAG, createFinalMessage); + Sentry.captureMessage(createFinalMessage, SentryLevel.INFO); if(status == STATUS_ERROR){ result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.ERROR, getResources().getString(R.string.FailedToCreateClaim)); }else{ result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.SUCCESS, null); } } catch (HttpException e){ + String errorMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_ERROR ts=%d thread=%s claimCode=%s errorClass=%s", + syncSessionId, System.currentTimeMillis(), uploadThread, claim.getClaimNumber(), e.getClass().getSimpleName() + ); + Log.i(TRACE_TAG, errorMessage); + Sentry.captureMessage(errorMessage, SentryLevel.ERROR); result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.ERROR, getResources().getString(R.string.SomethingWentWrongServer)); } results.add(result); @@ -184,13 +246,38 @@ private void handleUploadClaims() { } else { //upload claim try { + String claimUUID = sqlHandler.getClaimUUIDForCode(claim.getClaimNumber()); + long createStartTs = System.currentTimeMillis(); + String createMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_START ts=%d thread=%s claimCode=%s claimUUID=%s insureeId=%d programId=%d", + syncSessionId, createStartTs, uploadThread, claim.getClaimNumber(), claimUUID, insureeId, programId + ); + Log.i(TRACE_TAG, createMessage); + Sentry.captureMessage(createMessage, SentryLevel.INFO); Integer status = new CreateClaim().execute(claim, Integer.parseInt(adminId),Integer.parseInt(hfId),insureeId,programId, diagnosisId, programCode); + String endMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_END ts=%d thread=%s claimCode=%s claimUUID=%s status=%s durationMs=%d", + syncSessionId, System.currentTimeMillis(), uploadThread, claim.getClaimNumber(), claimUUID, + status == STATUS_ERROR ? "ERROR" : "SUCCESS", + System.currentTimeMillis() - createStartTs + ); + Log.i(TRACE_TAG, endMessage); + Sentry.captureMessage(endMessage, SentryLevel.INFO); if(status == STATUS_ERROR){ result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.ERROR, getResources().getString(R.string.FailedToCreateClaim)); }else{ result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.SUCCESS, null); } } catch (HttpException e){ + String errorMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=CREATECLAIM_EXECUTE_ERROR ts=%d thread=%s claimCode=%s errorClass=%s", + syncSessionId, System.currentTimeMillis(), uploadThread, claim.getClaimNumber(), e.getClass().getSimpleName() + ); + Log.i(TRACE_TAG, errorMessage); + Sentry.captureMessage(errorMessage, SentryLevel.ERROR); result = new PostNewClaims.Result(claim.getClaimNumber(), PostNewClaims.Result.Status.ERROR, getResources().getString(R.string.SomethingWentWrongServer)); } results.add(result); @@ -222,10 +309,24 @@ private void handleUploadClaims() { } } JSONArray claimStatus = processClaimResponse(results); + String uploadEndMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=UPLOAD_END ts=%d thread=%s durationMs=%d messagesCount=%d", + syncSessionId, System.currentTimeMillis(), uploadThread, System.currentTimeMillis() - uploadStartTs, claimStatus.length() + ); + Log.i(TRACE_TAG, uploadEndMessage); + Sentry.captureMessage(uploadEndMessage, SentryLevel.INFO); broadcastSyncSuccess(claimStatus); } catch (Exception e) { e.printStackTrace(); Sentry.captureException(e); + String uploadErrorMessage = String.format( + Locale.US, + "SYNC_TRACE session=%s event=UPLOAD_ERROR ts=%d thread=%s durationMs=%d errorClass=%s", + syncSessionId, System.currentTimeMillis(), uploadThread, System.currentTimeMillis() - uploadStartTs, e.getClass().getSimpleName() + ); + Log.i(TRACE_TAG, uploadErrorMessage); + Sentry.captureMessage(uploadErrorMessage, SentryLevel.ERROR); broadcastError(getResources().getString(R.string.ErrorOccurred) + ": " + e.getMessage(), ACTION_UPLOAD_CLAIMS); } } @@ -531,4 +632,4 @@ private void broadcastClaimCount(int entered, int accepted, int rejected) { sendBroadcast(resultIntent); Log.i(LOG_TAG, String.format(Locale.US, "%s finished with %s, result: p: %d,a: %d,r: %d", ACTION_CLAIM_COUNT, ACTION_CLAIM_COUNT_RESULT, entered, accepted, rejected)); } -} \ No newline at end of file +} diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/network/request/CreateClaimGraphQLRequest.java b/claimManagement/src/main/java/org/openimis/imisclaims/network/request/CreateClaimGraphQLRequest.java index 09783af1..5030ce16 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/network/request/CreateClaimGraphQLRequest.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/network/request/CreateClaimGraphQLRequest.java @@ -26,10 +26,14 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.Scope; public class CreateClaimGraphQLRequest extends BaseGraphQLRequest{ private static final String URI = BuildConfig.API_BASE_URL + "api/graphql"; + private static final String TRACE_TAG = "SYNC_TRACE"; public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); protected Global global; private final Token token = Global.getGlobal().getJWTToken(); @@ -44,29 +48,29 @@ public String create( @NonNull int programId, @NonNull int diagnosisId, @NonNull String programCode - ) throws Exception{ + ) throws Exception { String clientMutationId = UUID.randomUUID().toString(); String fagepFields = ""; - if(programCode.equals("PAL")){ + if (programCode.equals("PAL")) { fagepFields = " testNumber: \"" + claim.getTestNumber() + "\"" + " tdr: " + claim.getTdr(); } String claimServices = ""; - if(claim.getServices().size() == 0){ + if (claim.getServices().size() == 0) { claimServices = "[]"; - }else{ + } else { claimServices = "["; - for(Claim.Service service: claim.getServices()){ + for (Claim.Service service : claim.getServices()) { String subServices = ""; String subItems = ""; - if(service.getSubServices().size() == 0){ + if (service.getSubServices().size() == 0) { subServices = "[]"; - }else{ + } else { subServices = "["; - for (SubServiceItem subService: service.getSubServices()){ + for (SubServiceItem subService : service.getSubServices()) { String subObj = "{" + " subServiceCode: \"" + subService.getCode() + "\"" + " qtyAsked: \"" + subService.getQty() + "\"" @@ -78,11 +82,11 @@ public String create( subServices = subServices + "]"; } - if(service.getSubItems().size() == 0){ + if (service.getSubItems().size() == 0) { subItems = "[]"; - }else{ + } else { subItems = "["; - for(SubServiceItem subItem: service.getSubItems()){ + for (SubServiceItem subItem : service.getSubItems()) { String subObj = "{" + " subItemCode: \"" + subItem.getCode() + "\"" + " qtyAsked: \"" + subItem.getQty() + "\"" @@ -108,11 +112,11 @@ public String create( } String claimItems = ""; - if(claim.getMedications().size() == 0){ + if (claim.getMedications().size() == 0) { claimItems = "[]"; - }else{ + } else { claimItems = "["; - for(Claim.Medication item: claim.getMedications()){ + for (Claim.Medication item : claim.getMedications()) { String obj = "{" + " itemId: " + Integer.valueOf(item.getId()) + " priceAsked: \"" + item.getPrice() + "\"" @@ -139,7 +143,7 @@ public String create( + " healthFacilityId: " + hfId + " program: " + programId + " source: \"MOB\" " - + " visitType: \"" + claim.getVisitType() +"\"" + + " visitType: \"" + claim.getVisitType() + "\"" + fagepFields + " services: " + claimServices + " items: " + claimItems @@ -154,7 +158,7 @@ public String create( json.put("query", QUERY_DOCUMENT); - final TrustManager[] trustAllCerts = new TrustManager[] { + final TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { @@ -178,7 +182,7 @@ public java.security.cert.X509Certificate[] getAcceptedIssuers() { final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); OkHttpClient.Builder builder = new OkHttpClient.Builder(); - builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]); + builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); builder.hostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { @@ -192,20 +196,102 @@ public boolean verify(String hostname, SSLSession session) { Request request = new Request.Builder() .url(URI) - .addHeader("Authorization","bearer " + token.getTokenText().trim()) + .addHeader("Authorization", "bearer " + token.getTokenText().trim()) .post(body) .build(); + long httpStart = System.currentTimeMillis(); + String thread = Thread.currentThread().getName() + ":" + Thread.currentThread().getId(); + + String beforeHttpMessage = String.format( + "SYNC_TRACE session=- event=CREATECLAIM_BEFORE_HTTP ts=%d thread=%s claimCode=%s cmid=%s", + httpStart, + thread, + claim.getClaimNumber(), + clientMutationId + ); + + Log.i(TRACE_TAG, beforeHttpMessage); + + // Capture Sentry BEFORE HTTP + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CREATECLAIM_BEFORE_HTTP"); + scope.setExtra("claimCode", claim.getClaimNumber()); + scope.setExtra("clientMutationId", clientMutationId); + scope.setExtra("thread", thread); + scope.setExtra("timestamp", String.valueOf(httpStart)); + scope.setExtra("uri", URI); + }); + + Sentry.captureMessage(beforeHttpMessage, SentryLevel.INFO); + + try { Response response = httpClient.newCall(request).execute(); + int responseCode = response.code(); + long httpEnd = System.currentTimeMillis(); Log.i("HTTP_POST", URI + " - " + responseCode); Log.i("Claim", QUERY_DOCUMENT); + String afterHttpMessage = String.format( + "SYNC_TRACE session=- event=CREATECLAIM_AFTER_HTTP ts=%d thread=%s claimCode=%s cmid=%s httpCode=%d durationMs=%d", + httpEnd, + thread, + claim.getClaimNumber(), + clientMutationId, + responseCode, + httpEnd - httpStart + ); + + Log.i(TRACE_TAG, afterHttpMessage); + + // Capture Sentry AFTER HTTP + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CREATECLAIM_AFTER_HTTP"); + scope.setExtra("claimCode", claim.getClaimNumber()); + scope.setExtra("clientMutationId", clientMutationId); + scope.setExtra("thread", thread); + scope.setExtra("timestamp", String.valueOf(httpEnd)); + scope.setExtra("httpCode", String.valueOf(responseCode)); + scope.setExtra("durationMs", String.valueOf(httpEnd - httpStart)); + }); + + Sentry.captureMessage(afterHttpMessage, SentryLevel.INFO); + String responsePhrase = response.body().string(); + Log.i("RESPONSE", String.format("response: %d %s", responseCode, responsePhrase)); return clientMutationId; + + } catch (Exception e) { + + long errorTs = System.currentTimeMillis(); + + String errorMessage = String.format( + "SYNC_TRACE session=- event=CREATECLAIM_HTTP_EXCEPTION ts=%d thread=%s claimCode=%s cmid=%s error=%s", + errorTs, + thread, + claim.getClaimNumber(), + clientMutationId, + e.getMessage() + ); + + Log.e(TRACE_TAG, errorMessage); + + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CREATECLAIM_HTTP_EXCEPTION"); + scope.setExtra("claimCode", claim.getClaimNumber()); + scope.setExtra("clientMutationId", clientMutationId); + scope.setExtra("thread", thread); + scope.setExtra("timestamp", String.valueOf(errorTs)); + }); + + Sentry.captureException(e); + + throw e; + } } -} \ No newline at end of file +} diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CheckMutation.java b/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CheckMutation.java index 976b61ea..049738a7 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CheckMutation.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CheckMutation.java @@ -7,11 +7,16 @@ import org.json.JSONObject; import org.openimis.imisclaims.CheckMutationQuery; import org.openimis.imisclaims.network.request.CheckMutationGraphQLRequest; +import org.openimis.imisclaims.tools.Log; import java.util.concurrent.TimeoutException; +import io.sentry.Sentry; +import io.sentry.SentryLevel; + public class CheckMutation { + private static final String TRACE_TAG = "SYNC_TRACE"; private static final long DEFAULT_TIMEOUT = 600_000L; private static final long DEFAULT_DELAY = 1500L; private static final int STATUS_RECEIVED = 0; @@ -19,6 +24,7 @@ public class CheckMutation { private final long timeOutMs; private final long delayMs; + @NonNull private final CheckMutationGraphQLRequest request; @@ -38,45 +44,136 @@ public CheckMutation(long timeOutMs, long delayMs, @NonNull CheckMutationGraphQL @WorkerThread public Integer execute(@NonNull String uuid, @NonNull String message) throws Exception { + long start = System.currentTimeMillis(); + String thread = Thread.currentThread().getName() + ":" + Thread.currentThread().getId(); + + String startMsg = String.format( + "SYNC_TRACE session=- event=CHECKMUTATION_START ts=%d thread=%s cmid=%s", + start, thread, uuid + ); + + Log.i(TRACE_TAG, startMsg); + + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CHECKMUTATION_START"); + scope.setExtra("cmid", uuid); + scope.setExtra("thread", thread); + scope.setExtra("timestamp", String.valueOf(start)); + scope.setExtra("timeoutMs", String.valueOf(timeOutMs)); + }); + + Sentry.captureMessage(startMsg, SentryLevel.INFO); + CheckMutationQuery.Node node = null; - Integer status; - do { - if (node != null) { - Thread.sleep(delayMs); - } - node = request.execute(uuid); - status = node.status(); - if (System.currentTimeMillis() >= start + timeOutMs) { - throw new TimeoutException("Could not retrieve the mutation status of '" + uuid + "' within " + timeOutMs + "ms"); + Integer status = null; + + try { + + do { + if (node != null) { + Thread.sleep(delayMs); + } + + node = request.execute(uuid); + status = node.status(); + + long now = System.currentTimeMillis(); + + Integer finalStatus = status; + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CHECKMUTATION_POLL"); + scope.setExtra("cmid", uuid); + scope.setExtra("status", String.valueOf(finalStatus)); + scope.setExtra("elapsedMs", String.valueOf(now - start)); + }); + + if (now >= start + timeOutMs) { + throw new TimeoutException( + "Could not retrieve the mutation status of '" + uuid + "' within " + timeOutMs + "ms" + ); + } + + } while (status == null || status == STATUS_RECEIVED); + + long end = System.currentTimeMillis(); + + String doneMsg = String.format( + "SYNC_TRACE session=- event=CHECKMUTATION_DONE ts=%d thread=%s cmid=%s status=%s durationMs=%d", + end, thread, uuid, String.valueOf(status), end - start + ); + + Log.i(TRACE_TAG, doneMsg); + + Integer finalStatus1 = status; + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CHECKMUTATION_DONE"); + scope.setExtra("cmid", uuid); + scope.setExtra("status", String.valueOf(finalStatus1)); + scope.setExtra("durationMs", String.valueOf(end - start)); + }); + + Sentry.captureMessage(doneMsg, SentryLevel.INFO); + + if (status == STATUS_ERROR) { + String errorDetail = getErrorDetail(node.error()); + + IllegalStateException ex = + new IllegalStateException(message + ":\n" + errorDetail); + + Sentry.captureException(ex); + throw ex; } - } while (status == null || status == STATUS_RECEIVED); - if (status == STATUS_ERROR) { - throw new IllegalStateException(message + ":\n" + getErrorDetail(node.error())); - } + return status; + + } catch (Exception e) { + + long errorTs = System.currentTimeMillis(); - return status; + String errorMsg = String.format( + "SYNC_TRACE session=- event=CHECKMUTATION_EXCEPTION ts=%d thread=%s cmid=%s error=%s", + errorTs, thread, uuid, e.getMessage() + ); + + Log.e(TRACE_TAG, errorMsg); + + Sentry.configureScope(scope -> { + scope.setTag("sync_event", "CHECKMUTATION_EXCEPTION"); + scope.setExtra("cmid", uuid); + scope.setExtra("thread", thread); + scope.setExtra("timestamp", String.valueOf(errorTs)); + }); + + Sentry.captureException(e); + throw e; + } } private String getErrorDetail(String error) { try { JSONArray array = new JSONArray(error); StringBuilder builder = new StringBuilder(); - for (int i=0; i < array.length();i++) { + + for (int i = 0; i < array.length(); i++) { JSONObject object = array.getJSONObject(i); + if (builder.length() != 0) { builder.append("\n"); } + builder.append(" - "); builder.append(object.getString("detail")); } + if (builder.length() != 0) { return builder.toString(); } + } catch (Exception ignored) { // } + return error; } } \ No newline at end of file diff --git a/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CreateClaim.java b/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CreateClaim.java index 622942d7..a71cc799 100644 --- a/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CreateClaim.java +++ b/claimManagement/src/main/java/org/openimis/imisclaims/usecase/CreateClaim.java @@ -37,6 +37,6 @@ public Integer execute( ) throws Exception { return checkMutation.execute( createClaimGraphQLRequest.create(claim,hfId ,adminId, insureeId, programId, diagnosisId, programCode), - "Error while creating policy for beneficiary '" + claim.getClaimNumber() + "'"); + "Error while creating claim No '" + claim.getClaimNumber() + "'"); } }