diff --git a/pom.xml b/pom.xml
index dd4e04a0..04e4e450 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
com.iemr.mmu
mmu-api
- 3.9.0
+ 3.13.0
war
MMU-API
@@ -294,7 +294,12 @@
h2
runtime
-
+
+
+ software.amazon.awssdk
+ s3
+ 2.25.60
+
org.apache.commons
commons-csv
diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties
index 4462375e..de0f8d8e 100644
--- a/src/main/environment/common_ci.properties
+++ b/src/main/environment/common_ci.properties
@@ -5,6 +5,12 @@ spring.datasource.username=@env.DATABASE_USERNAME@
spring.datasource.password=@env.DATABASE_PASSWORD@
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+## S3 storage for diagnostic documents pushed from a van
+aws.s3.access-key=@env.AWS_S3_ACCESS_KEY@
+aws.s3.secret-key=@env.AWS_S3_SECRET_KEY@
+aws.s3.region=@env.AWS_S3_REGION@
+diagnostic.documents.s3.bucket=@env.DIAGNOSTIC_DOCUMENTS_S3_BUCKET@
+
## Carestream URLs, local
carestreamOrderCreateURL=@env.COMMON_API@carestream/createOrder
@@ -28,6 +34,11 @@ dataSyncUploadUrl=@env.MMU_CENTRAL_SERVER@dataSync/van-to-server
## Data download API, central
dataSyncDownloadUrl=@env.MMU_CENTRAL_SERVER@dataSync/server-to-van
+## Diagnostic document push (this server -> further central server)
+diagnosticDocumentUploadUrl=@env.MMU_CENTRAL_SERVER@dataSync/diagnostic-documents
+diagnosticDocument.push.batchSize=@env.DIAGNOSTIC_DOCUMENT_PUSH_BATCH_SIZE@
+diagnostic.documents.storage-root=@env.DIAGNOSTIC_DOCUMENTS_STORAGE_ROOT@
+
## TC specialist slot booking, # not required
tcSpecialistSlotBook=@env.TM_API@schedule/bookSlot
@@ -99,3 +110,9 @@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@
# Server that hosts the failedBeneficiaries API
nikshay.failedBeneficiariesUrl=@env.MMU_CENTRAL_SERVER@nikshay/syncStatus/failedBeneficiaries
+
+## BenID auto top-up URLs (feature stays off: stoptb.* not set for these builds)
+benCheckUrlLocal=@env.IDENTITY_API_BASE_URL@id/checkAvailablBenIDLocalServer
+centralLoginUrl=@env.COMMON_API_OPEN_SYNC@user/userAuthenticate
+centralRefreshTokenUrl=@env.COMMON_API_OPEN_SYNC@user/refreshToken
+centralSessionUrl=@env.COMMON_API_OPEN_SYNC@user/getLoginResponse
diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties
index 11a36cd6..c0ccf12e 100644
--- a/src/main/environment/common_docker.properties
+++ b/src/main/environment/common_docker.properties
@@ -5,6 +5,12 @@ spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+## S3 storage for diagnostic documents pushed from a van
+aws.s3.access-key=${AWS_S3_ACCESS_KEY:}
+aws.s3.secret-key=${AWS_S3_SECRET_KEY:}
+aws.s3.region=${AWS_S3_REGION:ap-south-1}
+diagnostic.documents.s3.bucket=${DIAGNOSTIC_DOCUMENTS_S3_BUCKET:}
+
## Carestream URLs, local
carestreamOrderCreateURL=${COMMON_API}carestream/createOrder
@@ -28,6 +34,11 @@ dataSyncUploadUrl=${MMU_CENTRAL_SERVER}dataSync/van-to-server
## Data download API, central
dataSyncDownloadUrl=${MMU_CENTRAL_SERVER}dataSync/server-to-van
+## Diagnostic document push (this server -> further central server)
+diagnosticDocumentUploadUrl=${MMU_CENTRAL_SERVER}dataSync/diagnostic-documents
+diagnosticDocument.push.batchSize=${DIAGNOSTIC_DOCUMENT_PUSH_BATCH_SIZE:3}
+diagnostic.documents.storage-root=${DIAGNOSTIC_DOCUMENTS_STORAGE_ROOT}
+
## TC specialist slot booking, # not required
tcSpecialistSlotBook=${TM_API}schedule/bookSlot
@@ -97,3 +108,17 @@ springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED}
# Server that hosts the failedBeneficiaries API
nikshay.failedBeneficiariesUrl=${MMU_CENTRAL_SERVER}nikshay/syncStatus/failedBeneficiaries
+
+## BenID auto top-up (StopTB laptops only; off unless STOPTB_ENFORCE_VANID=true and STOPTB_VAN_ID>0)
+benCheckUrlLocal=${IDENTITY_API_BASE_URL}id/checkAvailablBenIDLocalServer
+centralLoginUrl=${COMMON_API_OPEN_SYNC}user/userAuthenticate
+centralRefreshTokenUrl=${COMMON_API_OPEN_SYNC}user/refreshToken
+centralSessionUrl=${COMMON_API_OPEN_SYNC}user/getLoginResponse
+stoptb.enforce.vanid=${STOPTB_ENFORCE_VANID:false}
+stoptb.van.id=${STOPTB_VAN_ID:0}
+benid.pool.lowerLimit=${BENID_POOL_LOWER_LIMIT:2000}
+benid.pool.batchSize=${BENID_POOL_BATCH_SIZE:1000}
+benid.pool.bootstrapSize=${BENID_POOL_BOOTSTRAP_SIZE:10000}
+benid.token.dir=${BENID_TOKEN_DIR:/opt/amrit/data}
+benid.autoTopUp.initialDelayMs=${BENID_AUTO_TOPUP_INITIAL_DELAY_MS:120000}
+benid.autoTopUp.checkIntervalMs=${BENID_AUTO_TOPUP_CHECK_INTERVAL_MS:300000}
diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties
index 92911067..3f6dd822 100644
--- a/src/main/environment/common_example.properties
+++ b/src/main/environment/common_example.properties
@@ -7,6 +7,12 @@ spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+## S3 storage for diagnostic documents pushed from a van (POST /dataSync/diagnostic-documents)
+aws.s3.access-key=
+aws.s3.secret-key=
+aws.s3.region=ap-south-1
+diagnostic.documents.s3.bucket=
+
## Carestream URLs, local
#carestreamOrderCreateURL =http://localhost:1040/carestream/createOrder
carestreamOrderCreateURL =http://localhost:8083/carestream/createOrder
@@ -44,6 +50,10 @@ dataSyncUploadUrl=http://10.208.122.38:8080/mmu-api/dataSync/van-to-server
#dataSyncDownloadUrl=http://localhost:82/dataSync/server-to-van
dataSyncDownloadUrl=http://10.208.122.38:8080/mmu-api/dataSync/server-to-van
+diagnosticDocumentUploadUrl=http://localhost:8087/mmu-api/dataSync/diagnostic-documents
+diagnosticDocument.push.batchSize=3
+diagnostic.documents.storage-root=
+
## TC specialist slot booking, # not required
#tcSpecialistSlotBook=http://localhost:8080/schedule/bookSlot
tcSpecialistSlotBook=http://10.208.122.38:8080/tm-api/schedule/bookSlot
@@ -112,4 +122,32 @@ logging.path=logs/
logging.file.name=logs/mmu-api.log
-cors.allowed-origins=http://localhost:*
\ No newline at end of file
+cors.allowed-origins=http://localhost:*
+
+## BenID auto top-up (StopTB laptops only)
+benCheckUrlLocal=http://localhost:8094/id/checkAvailablBenIDLocalServer
+centralLoginUrl=https://uatstoptb.piramalswasthya.org/common-api/user/userAuthenticate
+centralRefreshTokenUrl=https://uatstoptb.piramalswasthya.org/common-api/user/refreshToken
+centralSessionUrl=https://uatstoptb.piramalswasthya.org/common-api/user/getLoginResponse
+stoptb.enforce.vanid=false
+stoptb.van.id=0
+benid.pool.lowerLimit=2000
+benid.pool.batchSize=1000
+benid.pool.bootstrapSize=10000
+benid.token.dir=/opt/amrit/data
+benid.autoTopUp.initialDelayMs=120000
+benid.autoTopUp.checkIntervalMs=300000
+
+## Automatic Data Sync (StopTB laptops only; needs the BenID central connection above). Off by default: manual sync only.
+sync.autoUpload.enabled=false
+sync.autoUpload.intervalMs=1800000
+sync.autoMasterDownload.enabled=false
+sync.autoMasterDownload.intervalMs=21600000
+# automatic master download fetches only inventory masters (items, stores, item->store mapping); false = all masters
+sync.autoMasterDownload.inventoryOnly=true
+sync.autoInventoryDownload.enabled=false
+
+## Central upload: replace van IDs in link columns (BenVisitID, PrescriptionID) by central IDs.
+## Off by default (other MMU servers unchanged); on for the StopTB central.
+## Env alternative: DATASYNC_TRANSLATEVANLINKS=true
+dataSync.translateVanLinks=false
diff --git a/src/main/java/com/iemr/mmu/Application.java b/src/main/java/com/iemr/mmu/Application.java
index 712d34f5..8fb40ccf 100644
--- a/src/main/java/com/iemr/mmu/Application.java
+++ b/src/main/java/com/iemr/mmu/Application.java
@@ -29,11 +29,13 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.scheduling.annotation.EnableScheduling;
import com.iemr.mmu.data.login.Users;
import com.iemr.mmu.utils.IEMRApplBeans;
@SpringBootApplication
+@EnableScheduling
public class Application {
public static void main(String[] args) {
diff --git a/src/main/java/com/iemr/mmu/config/S3ClientConfig.java b/src/main/java/com/iemr/mmu/config/S3ClientConfig.java
new file mode 100644
index 00000000..eb3509bf
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/config/S3ClientConfig.java
@@ -0,0 +1,53 @@
+package com.iemr.mmu.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+
+/***
+ * @purpose S3 client beans used to store diagnostic documents pushed from a van. Built
+ * once and shared, rather than re-built per request.
+ */
+@Configuration
+public class S3ClientConfig {
+
+ @Value("${aws.s3.access-key}")
+ private String accessKey;
+
+ @Value("${aws.s3.secret-key}")
+ private String secretKey;
+
+ @Value("${aws.s3.region}")
+ private String region;
+
+ /*
+ * Only the central server talks to S3; a van (camp laptop) runs the same war but never
+ * calls it, so it must be able to start without AWS keys. AwsBasicCredentials rejects blank
+ * keys at construction time, so fall back to the SDK default chain, which is resolved lazily
+ * on the first S3 call rather than at startup.
+ */
+ private AwsCredentialsProvider credentialsProvider() {
+ if (accessKey == null || accessKey.isBlank() || secretKey == null || secretKey.isBlank()) {
+ return DefaultCredentialsProvider.create();
+ }
+ return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey));
+ }
+
+ @Bean
+ public S3Client diagnosticDocumentS3Client() {
+ return S3Client.builder().region(Region.of(region)).credentialsProvider(credentialsProvider()).build();
+ }
+
+ @Bean
+ public S3Presigner diagnosticDocumentS3Presigner() {
+ return S3Presigner.builder().region(Region.of(region)).credentialsProvider(credentialsProvider()).build();
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java b/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java
new file mode 100644
index 00000000..71533a5d
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java
@@ -0,0 +1,94 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.controller.benid;
+
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.iemr.mmu.service.benid.BenIdAutoTopUpService;
+import com.iemr.mmu.service.benid.CentralAuthService;
+import com.iemr.mmu.utils.response.OutputResponse;
+
+import io.swagger.v3.oas.annotations.Operation;
+
+@RestController
+@RequestMapping(value = "/centralAuth", headers = "Authorization", produces = "application/json")
+public class CentralAuthController {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+
+ @Autowired
+ private CentralAuthService centralAuthService;
+
+ @Autowired
+ private BenIdAutoTopUpService benIdAutoTopUpService;
+
+ @Operation(summary = "Automatic BenID top-up and central connection status")
+ @GetMapping(value = "/status")
+ public String status() {
+ OutputResponse response = new OutputResponse();
+ try {
+ response.setResponse(benIdAutoTopUpService.status().toString());
+ } catch (Exception e) {
+ logger.error("Error reading BenID auto top-up status: " + e.getMessage());
+ response.setError(e);
+ }
+ return response.toString();
+ }
+
+ /**
+ * Called by MMU-UI right after a successful login. Returns immediately; the
+ * central login and any top-up happen in the background.
+ */
+ @Operation(summary = "Connect this laptop to central with the user's login (one time)")
+ @PostMapping(value = "/capture", consumes = "application/json")
+ public String capture(@RequestBody String requestOBJ) {
+ OutputResponse response = new OutputResponse();
+ try {
+ if (!centralAuthService.isEnabled()) {
+ response.setResponse("disabled");
+ return response.toString();
+ }
+ JSONObject obj = new JSONObject(requestOBJ);
+ String userName = obj.optString("userName", null);
+ String password = obj.optString("password", null);
+ boolean force = obj.optBoolean("reconnect", false);
+ benIdAutoTopUpService.runInBackground(() -> {
+ if (centralAuthService.capture(userName, password, force)) {
+ benIdAutoTopUpService.triggerNow();
+ }
+ });
+ response.setResponse("accepted");
+ } catch (Exception e) {
+ logger.error("Error in central capture: " + e.getMessage());
+ response.setError(e);
+ }
+ return response.toString();
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java b/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java
index a7f2a7c7..973fdf41 100644
--- a/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java
+++ b/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java
@@ -21,6 +21,8 @@
*/
package com.iemr.mmu.controller.dataSyncActivity;
+import java.util.Map;
+
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,13 +33,18 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.google.gson.Gson;
+import com.iemr.mmu.service.benid.BenIdAutoTopUpService;
import com.iemr.mmu.service.dataSyncActivity.DownSyncDataFromServerImpl;
import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerImpl;
import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerTransactionalImpl;
+import com.iemr.mmu.service.dataSyncActivity.AutoSyncService;
import com.iemr.mmu.service.dataSyncActivity.UploadDataToServerImpl;
+import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentFetchService;
+import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentPushServiceImpl;
import com.iemr.mmu.utils.CookieUtil;
import com.iemr.mmu.utils.response.OutputResponse;
@@ -60,7 +67,18 @@ public class StartSyncActivity {
@Autowired
private DownloadDataFromServerTransactionalImpl downloadDataFromServerTransactionalImpl;
@Autowired
+ private DiagnosticDocumentPushServiceImpl diagnosticDocumentPushServiceImpl;
+ @Autowired
+ private DiagnosticDocumentFetchService diagnosticDocumentFetchService;
+ @Autowired
private DownSyncDataFromServerImpl downSyncDataFromServerImpl;
+
+ @Autowired
+ private BenIdAutoTopUpService benIdAutoTopUpService;
+
+ @Autowired
+ private AutoSyncService autoSyncService;
+
private static final String GROUP_ID = "groupID";
private static final String PROVIDER_SERVICE_MAP_ID = "providerServiceMapID";
@@ -79,6 +97,8 @@ public String dataSyncToServer(@RequestBody String requestOBJ,
serverAuthorization, jwtToken);
// if (s != null)
response.setResponse(s);
+ // internet is up right now: top up the BenID pool if it is low
+ benIdAutoTopUpService.triggerNow();
// else
// response.setError(5000, "Error in data sync");
} else {
@@ -91,6 +111,45 @@ public String dataSyncToServer(@RequestBody String requestOBJ,
return response.toStringWithSerialization();
}
+ @Operation(summary = "Push pending diagnostic documents (docsProcessed='N') to the further central server, in batches")
+ @PostMapping(value = { "/diagnostic-documents-to-server" })
+ public String diagnosticDocumentsToServer(@RequestHeader(value = "Authorization") String authorization,
+ @RequestHeader(value = "ServerAuthorization") String serverAuthorization,
+ @RequestParam(required = false) Long villageId) {
+ OutputResponse response = new OutputResponse();
+ try {
+ String s = diagnosticDocumentPushServiceImpl.pushPendingDocuments(serverAuthorization);
+ if (s != null)
+ response.setResponse(s);
+ else
+ response.setError(5000, "Error in diagnostic document push");
+ } catch (Exception e) {
+ logger.error("Error in diagnostic document push : " + e);
+ response.setError(e);
+ }
+ return response.toString();
+ }
+
+ @Operation(summary = "Fetch a short-lived download URL for the latest successfully-pushed diagnostic document matching a beneficiary+documentType, from this van's own local record")
+ @GetMapping(value = { "/diagnostic-documents/download" })
+ public String diagnosticDocumentDownloadUrl(@RequestParam Long beneficiaryId, @RequestParam String documentType,
+ @RequestHeader(value = "Authorization") String authorization) {
+ OutputResponse response = new OutputResponse();
+ try {
+ Map download = diagnosticDocumentFetchService.getLatestDocumentDownload(beneficiaryId,
+ documentType);
+ if (download != null)
+ response.setResponse(new Gson().toJson(download));
+ else
+ response.setError(5000, "No pushed document found for beneficiaryId=" + beneficiaryId
+ + ", documentType=" + documentType);
+ } catch (Exception e) {
+ logger.error("Error fetching diagnostic document download URL : " + e);
+ response.setError(e);
+ }
+ return response.toString();
+ }
+
@Operation(summary = "Get data sync group details")
@GetMapping(value = { "/getSyncGroupDetails" })
public String getSyncGroupDetails() {
@@ -131,8 +190,11 @@ public String startMasterDownload(@RequestBody String requestOBJ,
if (s.equalsIgnoreCase("inProgress"))
response.setError(5000,
"Download is already in progress from different device, kindly wait to finish this");
- else
+ else {
response.setResponse(s);
+ // internet is up right now: top up the BenID pool if it is low
+ benIdAutoTopUpService.triggerNow();
+ }
} else
response.setError(5000, s);
} else {
@@ -147,6 +209,19 @@ public String startMasterDownload(@RequestBody String requestOBJ,
return response.toString();
}
+ @Operation(summary = "Automatic data sync status and the rows central rejected on upload")
+ @GetMapping(value = { "/autoSyncStatus" })
+ public String autoSyncStatus() {
+ OutputResponse response = new OutputResponse();
+ try {
+ response.setResponse(autoSyncService.status().toString());
+ } catch (Exception e) {
+ logger.error("Error while getting automatic sync status : " + e);
+ response.setError(e);
+ }
+ return response.toString();
+ }
+
@Operation(summary = "Data sync master download progress check")
@GetMapping(value = { "/checkMastersDownloadProgress" })
public String checkMastersDownloadProgress() {
diff --git a/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java b/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java
index 589e1dcb..1d2c8790 100644
--- a/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java
+++ b/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java
@@ -24,7 +24,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -35,11 +34,11 @@
import com.iemr.mmu.data.syncActivity_syncLayer.DownSyncDataDigester;
import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster;
import com.iemr.mmu.data.syncActivity_syncLayer.SyncUploadDataDigester;
+import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentIngestService;
import com.iemr.mmu.service.dataSyncLayerCentral.FetchDownloadDataImpl;
import com.iemr.mmu.service.dataSyncLayerCentral.GetDataFromVanAndSyncToDBImpl;
import com.iemr.mmu.service.dataSyncLayerCentral.GetDownSyncDataFromCentralImpl;
import com.iemr.mmu.service.dataSyncLayerCentral.GetMasterDataFromCentralForVanImpl;
-import com.iemr.mmu.utils.CookieUtil;
import com.iemr.mmu.utils.response.OutputResponse;
import io.swagger.v3.oas.annotations.Operation;
@@ -61,6 +60,8 @@ public class MMUDataSyncVanToServer {
private FetchDownloadDataImpl fetchDownloadDataImpl;
@Autowired
private GetDownSyncDataFromCentralImpl getDownSyncDataFromCentralImpl;
+ @Autowired
+ private DiagnosticDocumentIngestService diagnosticDocumentIngestService;
@Operation(summary = "Sync data from van-to-server")
@PostMapping(value = { "/van-to-server" }, consumes = "application/json", produces = "application/json")
@@ -81,6 +82,24 @@ public String dataSyncToServer(@RequestBody String requestOBJ,
return response.toString();
}
+ @Operation(summary = "Receive diagnostic documents pushed from a van and store each in S3 (no database write here)")
+ @PostMapping(value = { "/diagnostic-documents" }, consumes = "application/json", produces = "application/json")
+ public String diagnosticDocumentsFromVan(@RequestBody String requestOBJ,
+ @RequestHeader(value = "Authorization") String Authorization) {
+ OutputResponse response = new OutputResponse();
+ try {
+ String s = diagnosticDocumentIngestService.ingestDocuments(requestOBJ);
+ if (s != null)
+ response.setResponse(s);
+ else
+ response.setError(5000, "diagnostic document ingest failed");
+ } catch (Exception e) {
+ response.setError(e);
+ logger.error("Diagnostic document ingest Exception" + e);
+ }
+ return response.toString();
+ }
+
@Operation(summary = "Download data from server-to-van")
@PostMapping(value = { "/server-to-van" }, consumes = "application/json", produces = "application/json")
public String dataDownloadFromServer(@RequestBody SyncDownloadMaster syncDownloadMaster,
diff --git a/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java b/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java
index 5302188d..fa3bd7f5 100644
--- a/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java
+++ b/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java
@@ -146,8 +146,10 @@ public record NikshayRawRow(Long benRegId, String firstName, String middleLastNa
+ " d.incomeStatus AS socioeconomicStatus, "
+ " (SELECT o.chief_complaint FROM tb_stoptb_general_opd o WHERE o.ben_reg_id = m.BenRegId "
+ " AND o.deleted = 0 ORDER BY o.id DESC LIMIT 1) AS chiefComplaint, "
- + " (SELECT ge.hiv_status FROM tb_stoptb_general_examination ge WHERE ge.beneficiary_reg_id = m.BenRegId "
- + " AND ge.deleted = 0 ORDER BY ge.id DESC LIMIT 1) AS hivStatus, "
+ // hiv_status used to live on tb_stoptb_general_examination too, but that copy was
+ // dropped (AMRIT-DB V109) once tb_screening became the single source of truth.
+ + " (SELECT s.hiv_status FROM tb_screening s WHERE s.ben_reg_id = m.BenRegId "
+ + " AND (s.deleted = 0 OR s.deleted IS NULL) ORDER BY s.id DESC LIMIT 1) AS hivStatus, "
+ " d.IsHIVPositive AS isHivPos, "
+ " d.nikshayId AS existingNikshayId "
+ "FROM db_identity.i_beneficiarymapping m "
diff --git a/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java b/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java
index c146a7a9..dd054c41 100644
--- a/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java
+++ b/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java
@@ -31,4 +31,6 @@
@Repository
public interface SyncUtilityClassRepo extends CrudRepository {
List findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(Integer groupID, Boolean deleted);
+
+ List findByDeletedOrderBySyncTableDetailID(Boolean deleted);
}
diff --git a/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java b/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java
new file mode 100644
index 00000000..56b78f16
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java
@@ -0,0 +1,372 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.benid;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import com.iemr.mmu.utils.RestTemplateUtil;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+
+/**
+ * Keeps the local BenID pool filled without anyone clicking "Generate BenID".
+ *
+ * Runs only on StopTB laptops (stoptb.enforce.vanid=true and stoptb.van.id>0).
+ * Pool empty -> bootstrap size; pool below lower limit -> batch size. IDs
+ * received from central are written to a pending file before the local import,
+ * so a failed import is retried with the same IDs instead of reserving new ones.
+ */
+@Service
+public class BenIdAutoTopUpService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+
+ /** Shared with the manual "Generate BenID" button so both never run together. */
+ public static final ReentrantLock GENERATION_LOCK = new ReentrantLock();
+
+ private static final String PENDING_FILE_NAME = "benid-pending-import.json";
+ private static final long TOKEN_KEEP_ALIVE_MS = 24 * 60 * 60_000L;
+
+ @Autowired
+ private CentralAuthService centralAuthService;
+
+ @Value("${benGenUrlCentral:}")
+ private String benGenUrlCentral;
+
+ @Value("${benImportUrlLocal:}")
+ private String benImportUrlLocal;
+
+ @Value("${benCheckUrlLocal:}")
+ private String benCheckUrlLocal;
+
+ @Value("${benid.pool.lowerLimit:2000}")
+ private long lowerLimit;
+
+ @Value("${benid.pool.batchSize:1000}")
+ private long batchSize;
+
+ @Value("${benid.pool.bootstrapSize:10000}")
+ private long bootstrapSize;
+
+ // central is asked for at most this many IDs per request: a big request can outlast the
+ // central gateway timeout, and a reply lost after central created the IDs wastes them
+ @Value("${benid.pool.maxPerRequest:1000}")
+ private long maxPerRequest;
+
+ @Value("${benid.token.dir:/opt/amrit/data}")
+ private String dataDir;
+
+ // wait after the 1st, 2nd, 3rd and later failures (5, 15, 30, 60 minutes)
+ @Value("${benid.autoTopUp.retryDelaysMs:300000,900000,1800000,3600000}")
+ private long[] retryDelaysMs;
+
+ @Value("${benid.http.connectTimeoutMs:15000}")
+ private int connectTimeoutMs;
+
+ @Value("${benid.http.readTimeoutMs:60000}")
+ private int readTimeoutMs;
+
+ private RestTemplate restTemplate;
+
+ private volatile int failureCount;
+ private volatile long nextAttemptAt;
+ private volatile long lastPoolCount = -1;
+ private volatile long lastTopUpAt;
+ private volatile long lastTopUpCount;
+ private volatile String lastError;
+ private volatile int pendingImportFailures;
+
+ // one background thread for this feature, so network waits never hold shared pool threads
+ private final ExecutorService background = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "benid-auto-topup");
+ t.setDaemon(true);
+ return t;
+ });
+
+ private static final int MAX_PENDING_IMPORT_FAILURES = 3;
+
+ @PostConstruct
+ public void init() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(connectTimeoutMs);
+ factory.setReadTimeout(readTimeoutMs);
+ restTemplate = new RestTemplate(factory);
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ background.shutdownNow();
+ }
+
+ /** Runs a task on this feature's own background thread. */
+ public void runInBackground(Runnable task) {
+ background.submit(task);
+ }
+
+ /** Regular check; does nothing on central or non-StopTB deployments. */
+ @Scheduled(initialDelayString = "${benid.autoTopUp.initialDelayMs:120000}", fixedDelayString = "${benid.autoTopUp.checkIntervalMs:300000}")
+ public void scheduledCheck() {
+ runOnce(false);
+ }
+
+ /** Run straight away (after a login or a Data Sync), in the background. */
+ public void triggerNow() {
+ if (centralAuthService.isEnabled()) {
+ background.submit(() -> runOnce(true));
+ }
+ }
+
+ private void runOnce(boolean ignoreBackoff) {
+ if (!centralAuthService.isEnabled()) {
+ return;
+ }
+ if (!ignoreBackoff && System.currentTimeMillis() < nextAttemptAt) {
+ return;
+ }
+ if (!GENERATION_LOCK.tryLock()) {
+ logger.info("BenID generation already running - skipping this check");
+ return;
+ }
+ try {
+ // IDs already received but not saved locally are imported first
+ if (importPendingIfAny()) {
+ onSuccess();
+ }
+
+ long available = countLocalPool();
+ lastPoolCount = available;
+ long needed = available <= 0 ? bootstrapSize : (available < lowerLimit ? batchSize : 0);
+ if (needed == 0) {
+ centralAuthService.keepAliveIfDue(TOKEN_KEEP_ALIVE_MS);
+ onSuccess();
+ return;
+ }
+
+ String key = centralAuthService.getCentralSessionKey();
+ if (key == null) {
+ lastError = "No central connection - waiting for a user login";
+ logger.warn("BenID pool low ({} for vanID {}) but no central token - waiting for a user login",
+ available, centralAuthService.getVanID());
+ return;
+ }
+
+ int imported = 0;
+ for (long remaining = needed; remaining > 0;) {
+ long chunk = Math.min(remaining, Math.max(1, maxPerRequest));
+ imported += generateAndImport(centralAuthService.getVanID(), chunk, key, key, "datasync");
+ remaining -= chunk;
+ }
+ lastPoolCount = countLocalPool();
+ lastTopUpAt = System.currentTimeMillis();
+ lastTopUpCount = imported;
+ logger.info("BenID auto top-up: vanID {} +{} IDs, pool now {}", centralAuthService.getVanID(), imported,
+ lastPoolCount);
+ onSuccess();
+ } catch (Exception e) {
+ onFailure(e);
+ } finally {
+ GENERATION_LOCK.unlock();
+ }
+ }
+
+ /**
+ * Asks central for IDs for the van and imports them locally. The central
+ * response is written to the pending file first, so a failed local import
+ * does not lose IDs that central has already reserved for this van.
+ *
+ * @return number of IDs imported
+ */
+ public int generateAndImport(int vanID, long count, String centralAuthorization, String localAuthorization,
+ String localToken) throws Exception {
+ JSONObject request = new JSONObject();
+ request.put("vanID", vanID);
+ request.put("benIDRequired", count);
+
+ HttpEntity centralRequest = RestTemplateUtil.createRequestEntity(request.toString(),
+ centralAuthorization, "datasync");
+ logger.info("Requesting {} BenIDs from central for vanID {}", count, vanID);
+ long start = System.currentTimeMillis();
+ ResponseEntity response = restTemplate.exchange(benGenUrlCentral, HttpMethod.POST, centralRequest,
+ String.class);
+ logger.info("Central BenID generation answered in {} ms", System.currentTimeMillis() - start);
+ JSONObject obj = new JSONObject(response.getBody());
+ if (obj.optInt("statusCode") != 200 || !obj.has("data")) {
+ throw new IllegalStateException("Central BenID generation failed: " + obj.optString("errorMessage"));
+ }
+ JSONArray ids = obj.getJSONArray("data");
+ for (int k = 0; k < ids.length(); k++) {
+ ids.getJSONObject(k).put("vanID", vanID);
+ }
+ writePending(ids.toString());
+ long importStart = System.currentTimeMillis();
+ importLocally(ids.toString(), localAuthorization, localToken);
+ deletePending();
+ logger.info("Imported {} BenIDs locally in {} ms", ids.length(), System.currentTimeMillis() - importStart);
+ return ids.length();
+ }
+
+ private boolean importPendingIfAny() throws Exception {
+ Path pending = pendingPath();
+ if (!Files.exists(pending)) {
+ return false;
+ }
+ logger.info("Importing BenIDs left from an earlier run");
+ try {
+ String payload = new String(Files.readAllBytes(pending), StandardCharsets.UTF_8);
+ importLocally(payload, "datasync", "datasync");
+ } catch (Exception e) {
+ pendingImportFailures++;
+ if (pendingImportFailures < MAX_PENDING_IMPORT_FAILURES) {
+ throw e;
+ }
+ // a file that keeps failing (e.g. half-written at a power cut) must not stop top-ups
+ Path setAside = Paths.get(dataDir, "benid-pending-import.failed-" + System.currentTimeMillis() + ".json");
+ Files.move(pending, setAside, StandardCopyOption.REPLACE_EXISTING);
+ pendingImportFailures = 0;
+ logger.warn("Pending BenID file could not be imported {} times; moved to {} and continuing: {}",
+ MAX_PENDING_IMPORT_FAILURES, setAside, e.getMessage());
+ return false;
+ }
+ pendingImportFailures = 0;
+ deletePending();
+ return true;
+ }
+
+ private void importLocally(String payload, String authorization, String token) {
+ HttpEntity localRequest = RestTemplateUtil.createRequestEntity(payload, authorization, token);
+ ResponseEntity response = restTemplate.exchange(benImportUrlLocal, HttpMethod.POST, localRequest,
+ String.class);
+ JSONObject obj = new JSONObject(response.getBody());
+ if (obj.optInt("statusCode") != 200) {
+ throw new IllegalStateException("Local BenID import failed: " + obj.optString("errorMessage"));
+ }
+ }
+
+ public long countLocalPool() {
+ HttpEntity request = RestTemplateUtil.createRequestEntity(null, "datasync", "datasync");
+ ResponseEntity response = restTemplate.exchange(benCheckUrlLocal, HttpMethod.GET, request,
+ String.class);
+ JSONObject obj = new JSONObject(response.getBody());
+ if (obj.optInt("statusCode") != 200) {
+ throw new IllegalStateException("Local BenID count failed: " + obj.optString("errorMessage"));
+ }
+ Object data = obj.get("data");
+ JSONObject dataObj = data instanceof JSONObject ? (JSONObject) data : new JSONObject(data.toString());
+ return Long.parseLong(dataObj.get("response").toString());
+ }
+
+ private void onSuccess() {
+ failureCount = 0;
+ nextAttemptAt = 0;
+ lastError = null;
+ }
+
+ private void onFailure(Exception e) {
+ long base = retryDelaysMs[Math.min(failureCount, retryDelaysMs.length - 1)];
+ // +/-20% so that many laptops do not all retry at the same moment after an outage
+ long delay = base <= 0 ? 0 : (long) (base * ThreadLocalRandom.current().nextDouble(0.8, 1.2));
+ failureCount++;
+ nextAttemptAt = System.currentTimeMillis() + delay;
+ lastError = e.getMessage();
+ logger.warn("BenID auto top-up failed (attempt {}), next try in {} s: {}", failureCount, delay / 1000,
+ e.getMessage());
+ }
+
+ private Path pendingPath() {
+ return Paths.get(dataDir, PENDING_FILE_NAME);
+ }
+
+ private void writePending(String payload) {
+ try {
+ Files.createDirectories(Paths.get(dataDir));
+ // write a temp file and swap it in, so a power cut never leaves a half-written file
+ Path tmp = Paths.get(dataDir, PENDING_FILE_NAME + ".tmp");
+ Files.write(tmp, payload.getBytes(StandardCharsets.UTF_8));
+ try {
+ Files.move(tmp, pendingPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (java.nio.file.AtomicMoveNotSupportedException e) {
+ Files.move(tmp, pendingPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (Exception e) {
+ logger.warn("Could not write pending BenID file: {}", e.getMessage());
+ }
+ }
+
+ private void deletePending() {
+ try {
+ Files.deleteIfExists(pendingPath());
+ } catch (Exception e) {
+ logger.warn("Could not delete pending BenID file: {}", e.getMessage());
+ }
+ }
+
+ // ---------------------------------------------------------------- status
+
+ public JSONObject status() {
+ JSONObject s = new JSONObject();
+ s.put("enabled", centralAuthService.isEnabled());
+ if (!centralAuthService.isEnabled()) {
+ return s;
+ }
+ s.put("vanID", centralAuthService.getVanID());
+ s.put("centralConnection", centralAuthService.getState().name());
+ s.put("connectedUser", centralAuthService.getConnectedUser());
+ try {
+ lastPoolCount = countLocalPool();
+ } catch (Exception e) {
+ logger.debug("Live BenID count not available: {}", e.getMessage());
+ }
+ s.put("poolCount", lastPoolCount);
+ s.put("lowerLimit", lowerLimit);
+ s.put("lastTopUpAt", lastTopUpAt);
+ s.put("lastTopUpCount", lastTopUpCount);
+ s.put("pendingImport", Files.exists(pendingPath()));
+ String error = lastError != null ? lastError : centralAuthService.getLastError();
+ if (error != null) {
+ s.put("lastError", error);
+ }
+ return s;
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java b/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java
new file mode 100644
index 00000000..d065eb19
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java
@@ -0,0 +1,416 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.benid;
+
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestTemplate;
+
+import jakarta.annotation.PostConstruct;
+
+/**
+ * Keeps this laptop connected to the central server without storing any
+ * central password.
+ *
+ * The operator's normal laptop login is passed here once; it is used for a
+ * single central login as an API client, which returns a refresh token. Only
+ * that refresh token is kept (AES-GCM encrypted with a key tied to this
+ * machine). Each time a central session is needed the refresh token is
+ * exchanged for a new JWT + refresh token, and the JWT for a central session
+ * key.
+ */
+@Service
+public class CentralAuthService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+
+ public enum State {
+ DISABLED, NOT_CONNECTED, CONNECTED, RECONNECT_NEEDED, PASSWORD_MISMATCH
+ }
+
+ // Central refresh tokens are only issued to API (mobile) clients.
+ private static final String API_CLIENT_USER_AGENT = "okhttp/4.12.0";
+ private static final String TOKEN_FILE_NAME = "central-refresh-token";
+ // user name only (no password): a laptop password central rejected is not sent again,
+ // also after a restart, so the central account cannot get locked by repeated attempts
+ private static final String REJECTED_FILE_NAME = "central-login-rejected";
+
+ @Value("${stoptb.enforce.vanid:false}")
+ private boolean enforceVanID;
+
+ @Value("${stoptb.van.id:0}")
+ private int vanID;
+
+ @Value("${centralLoginUrl:}")
+ private String centralLoginUrl;
+
+ @Value("${centralRefreshTokenUrl:}")
+ private String centralRefreshTokenUrl;
+
+ @Value("${centralSessionUrl:}")
+ private String centralSessionUrl;
+
+ @Value("${benid.token.dir:/opt/amrit/data}")
+ private String tokenDir;
+
+ @Value("${benid.token.secret:amrit-mmu-benid}")
+ private String tokenSecret;
+
+ @Value("${benid.http.connectTimeoutMs:15000}")
+ private int connectTimeoutMs;
+
+ @Value("${benid.http.readTimeoutMs:60000}")
+ private int readTimeoutMs;
+
+ private RestTemplate restTemplate;
+
+ // in-memory copy; the file is only a durable backup of this value
+ private volatile String refreshToken;
+ private volatile State state = State.NOT_CONNECTED;
+ private volatile String lastError;
+ private volatile long lastRefreshAt;
+ private volatile String connectedUser;
+ private volatile String rejectedUser;
+
+ @PostConstruct
+ public void init() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(connectTimeoutMs);
+ factory.setReadTimeout(readTimeoutMs);
+ restTemplate = new RestTemplate(factory);
+
+ if (!isEnabled()) {
+ state = State.DISABLED;
+ logger.info("BenID auto top-up disabled (stoptb.enforce.vanid={}, stoptb.van.id={})", enforceVanID,
+ vanID);
+ return;
+ }
+ refreshToken = readTokenFile();
+ rejectedUser = readRejectedUser();
+ state = refreshToken != null ? State.CONNECTED : State.NOT_CONNECTED;
+ logger.info("BenID auto top-up enabled for vanID {}; central token {}", vanID,
+ refreshToken != null ? "loaded" : "not present yet");
+ }
+
+ public boolean isEnabled() {
+ return enforceVanID && vanID > 0 && !centralLoginUrl.isEmpty();
+ }
+
+ public int getVanID() {
+ return vanID;
+ }
+
+ public boolean hasToken() {
+ return refreshToken != null;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public String getLastError() {
+ return lastError;
+ }
+
+ public long getLastRefreshAt() {
+ return lastRefreshAt;
+ }
+
+ public String getConnectedUser() {
+ return connectedUser;
+ }
+
+ /**
+ * One central login with the operator's credentials (password already
+ * encrypted the way MMU-UI sends it to Common-API). Skipped when a working
+ * token already exists, so central is not hit on every laptop login. A
+ * rejected password is never retried, so the central account cannot get
+ * locked by this.
+ *
+ * @param force true when the user explicitly reconnects (e.g. Data Sync
+ * login), even if a token exists
+ * @return true if a new token was saved
+ */
+ public synchronized boolean capture(String userName, String encryptedPassword, boolean force) {
+ if (!isEnabled() || userName == null || encryptedPassword == null) {
+ return false;
+ }
+ if (!force && refreshToken != null && state == State.CONNECTED) {
+ return false;
+ }
+ // a laptop password central already rejected is not sent again (account lock risk);
+ // only a Data Sync login (force, real central password) tries again
+ if (!force && userName.trim().equalsIgnoreCase(rejectedUser)) {
+ logger.info("Central login skipped for user {}: password was rejected before", userName);
+ return false;
+ }
+ try {
+ JSONObject body = new JSONObject();
+ body.put("userName", userName.trim());
+ body.put("password", encryptedPassword);
+ body.put("doLogout", false);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set(HttpHeaders.USER_AGENT, API_CLIENT_USER_AGENT);
+
+ ResponseEntity response = restTemplate.exchange(centralLoginUrl, HttpMethod.POST,
+ new HttpEntity<>(body.toString(), headers), String.class);
+ JSONObject obj = new JSONObject(response.getBody());
+ if (obj.optInt("statusCode") == 200 && obj.has("data")
+ && obj.getJSONObject("data").has("refreshToken")) {
+ saveToken(obj.getJSONObject("data").getString("refreshToken"));
+ connectedUser = userName.trim();
+ rejectedUser = null;
+ deleteRejectedUser();
+ state = State.CONNECTED;
+ lastError = null;
+ logger.info("Central token captured for user {}", connectedUser);
+ return true;
+ }
+ // wrong username/password or account problem: do not retry with these credentials
+ lastError = obj.optString("errorMessage", "Central login rejected");
+ rejectedUser = userName.trim();
+ saveRejectedUser(rejectedUser);
+ state = State.PASSWORD_MISMATCH;
+ logger.warn("Central login rejected for user {}: {}", userName, lastError);
+ } catch (Exception e) {
+ // network problem: nothing saved, next login tries again
+ lastError = "Central server not reachable: " + e.getMessage();
+ logger.warn("Central login not possible right now: {}", e.getMessage());
+ }
+ return false;
+ }
+
+ /**
+ * Returns a fresh central session key (refreshToken -> JWT -> session key),
+ * or null if this laptop has no usable token. Throws on network errors so
+ * callers can retry later.
+ */
+ public synchronized String getCentralSessionKey() throws Exception {
+ if (!isEnabled() || refreshToken == null) {
+ return null;
+ }
+ String jwt;
+ long start = System.currentTimeMillis();
+ try {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ JSONObject body = new JSONObject();
+ body.put("refreshToken", refreshToken);
+ ResponseEntity response = restTemplate.exchange(centralRefreshTokenUrl, HttpMethod.POST,
+ new HttpEntity<>(body.toString(), headers), String.class);
+ JSONObject obj = new JSONObject(response.getBody());
+ jwt = obj.getString("jwtToken");
+ if (obj.has("refreshToken")) {
+ saveToken(obj.getString("refreshToken"));
+ }
+ lastRefreshAt = System.currentTimeMillis();
+ logger.info("Central token refreshed in {} ms", lastRefreshAt - start);
+ } catch (HttpStatusCodeException e) {
+ if (e.getStatusCode().value() == 401) {
+ // expired, revoked or user deactivated on central
+ logger.warn("Central refresh token rejected ({}); a new login is needed",
+ e.getResponseBodyAsString());
+ deleteToken();
+ state = State.RECONNECT_NEEDED;
+ lastError = "Central connection expired, please log in again";
+ return null;
+ }
+ throw e;
+ }
+
+ // JWT -> central session key; Common-API creates a new Redis session for it
+ long sessionStart = System.currentTimeMillis();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("Jwttoken", jwt);
+ ResponseEntity response = restTemplate.exchange(centralSessionUrl, HttpMethod.POST,
+ new HttpEntity<>("{}", headers), String.class);
+ JSONObject obj = new JSONObject(response.getBody());
+ if (obj.optInt("statusCode") != 200 || !obj.has("data")) {
+ throw new IllegalStateException("Central session not created: " + obj.optString("errorMessage"));
+ }
+ Object data = obj.get("data");
+ JSONObject dataObj = data instanceof JSONObject ? (JSONObject) data : new JSONObject(data.toString());
+ state = State.CONNECTED;
+ lastError = null;
+ logger.info("Central session created in {} ms", System.currentTimeMillis() - sessionStart);
+ return dataObj.getString("key");
+ }
+
+ /** Keeps the 90-day token alive even when no IDs are needed. */
+ public void keepAliveIfDue(long maxAgeMs) {
+ if (refreshToken != null && System.currentTimeMillis() - lastRefreshAt > maxAgeMs) {
+ try {
+ getCentralSessionKey();
+ } catch (Exception e) {
+ logger.debug("Token keep-alive skipped: {}", e.getMessage());
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------- token file
+
+ private Path tokenPath() {
+ return Paths.get(tokenDir, TOKEN_FILE_NAME);
+ }
+
+ private void saveToken(String token) {
+ refreshToken = token;
+ try {
+ Path dir = Paths.get(tokenDir);
+ Files.createDirectories(dir);
+ setPermissions(dir, "rwx------");
+ Path tmp = dir.resolve(TOKEN_FILE_NAME + ".tmp");
+ Files.write(tmp, encrypt(token).getBytes(StandardCharsets.UTF_8));
+ setPermissions(tmp, "rw-------");
+ try {
+ Files.move(tmp, tokenPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(tmp, tokenPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (Exception e) {
+ // keep working from memory; the token is captured again after a restart
+ logger.warn("Could not save central token to {} (kept in memory only): {}", tokenDir, e.getMessage());
+ }
+ }
+
+ private String readTokenFile() {
+ try {
+ Path path = tokenPath();
+ if (Files.exists(path)) {
+ return decrypt(new String(Files.readAllBytes(path), StandardCharsets.UTF_8).trim());
+ }
+ } catch (Exception e) {
+ logger.warn("Stored central token could not be read, a new login is needed: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ private void deleteToken() {
+ refreshToken = null;
+ try {
+ Files.deleteIfExists(tokenPath());
+ } catch (Exception e) {
+ logger.warn("Could not delete central token file: {}", e.getMessage());
+ }
+ }
+
+ private String readRejectedUser() {
+ try {
+ Path path = Paths.get(tokenDir, REJECTED_FILE_NAME);
+ if (Files.exists(path)) {
+ String user = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).trim();
+ return user.isEmpty() ? null : user;
+ }
+ } catch (Exception e) {
+ logger.warn("Could not read rejected-login marker: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ private void saveRejectedUser(String user) {
+ try {
+ Path dir = Paths.get(tokenDir);
+ Files.createDirectories(dir);
+ Path path = dir.resolve(REJECTED_FILE_NAME);
+ Files.write(path, user.getBytes(StandardCharsets.UTF_8));
+ setPermissions(path, "rw-------");
+ } catch (Exception e) {
+ logger.warn("Could not save rejected-login marker: {}", e.getMessage());
+ }
+ }
+
+ private void deleteRejectedUser() {
+ try {
+ Files.deleteIfExists(Paths.get(tokenDir, REJECTED_FILE_NAME));
+ } catch (Exception e) {
+ logger.warn("Could not delete rejected-login marker: {}", e.getMessage());
+ }
+ }
+
+ private static void setPermissions(Path path, String perms) {
+ try {
+ Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(perms));
+ } catch (Exception e) {
+ // non-POSIX file system; nothing more we can do
+ }
+ }
+
+ private SecretKeySpec machineKey() throws Exception {
+ String machineId;
+ try {
+ machineId = new String(Files.readAllBytes(Paths.get("/etc/machine-id")), StandardCharsets.UTF_8).trim();
+ } catch (Exception e) {
+ machineId = InetAddress.getLocalHost().getHostName();
+ }
+ byte[] key = MessageDigest.getInstance("SHA-256")
+ .digest((machineId + ":" + tokenSecret).getBytes(StandardCharsets.UTF_8));
+ return new SecretKeySpec(key, "AES");
+ }
+
+ private String encrypt(String plain) throws Exception {
+ byte[] iv = new byte[12];
+ new SecureRandom().nextBytes(iv);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, machineKey(), new GCMParameterSpec(128, iv));
+ byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
+ byte[] out = new byte[iv.length + enc.length];
+ System.arraycopy(iv, 0, out, 0, iv.length);
+ System.arraycopy(enc, 0, out, iv.length, enc.length);
+ return Base64.getEncoder().encodeToString(out);
+ }
+
+ private String decrypt(String encoded) throws Exception {
+ byte[] in = Base64.getDecoder().decode(encoded);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, machineKey(), new GCMParameterSpec(128, in, 0, 12));
+ return new String(cipher.doFinal(in, 12, in.length - 12), StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java
new file mode 100644
index 00000000..e8971fe7
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java
@@ -0,0 +1,239 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.dataSyncActivity;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.iemr.mmu.service.benid.BenIdAutoTopUpService;
+import com.iemr.mmu.service.benid.CentralAuthService;
+
+import jakarta.annotation.PreDestroy;
+
+/**
+ * StopTB laptops: runs Data Sync by itself, so central stays current and laptops get master changes without
+ * anyone pressing Upload or Download Masters.
+ *
+ * - Upload (van to central) every sync.autoUpload.intervalMs (default 30 min).
+ * - Master download (central to van) every sync.autoMasterDownload.intervalMs (default 6 h).
+ *
+ * Uses the central connection of the automatic BenID download (CentralAuthService): no password is stored, and
+ * SyncedBy is the operator who connected the laptop. Active only where CentralAuthService is enabled
+ * (stoptb.enforce.vanid=true and stoptb.van.id>0); central and other deployments are unaffected. The manual
+ * buttons keep working as before.
+ *
+ * Off by default (manual sync only) until the automatic schedule is approved: set sync.autoUpload.enabled,
+ * sync.autoMasterDownload.enabled and sync.autoInventoryDownload.enabled to true to switch it on.
+ */
+@Service
+public class AutoSyncService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+
+ /** Upload and master download never run at the same time from here. */
+ private static final ReentrantLock SYNC_LOCK = new ReentrantLock();
+
+ @Autowired
+ private CentralAuthService centralAuthService;
+
+ @Autowired
+ private UploadDataToServerImpl uploadDataToServerImpl;
+
+ @Autowired
+ private DownloadDataFromServerImpl downloadDataFromServerImpl;
+
+ @Autowired
+ private DownloadDataFromServerTransactionalImpl downloadDataFromServerTransactional;
+
+ @Autowired
+ private DataSyncRepository dataSyncRepository;
+
+ @Autowired
+ private BenIdAutoTopUpService benIdAutoTopUpService;
+
+ @Value("${sync.autoUpload.enabled:false}")
+ private boolean uploadEnabled;
+
+ @Value("${sync.autoMasterDownload.enabled:false}")
+ private boolean masterDownloadEnabled;
+
+ // refills / stock issued at central for this van's store ("Inventory Data Download"), after each upload
+ @Value("${sync.autoInventoryDownload.enabled:false}")
+ private boolean inventoryDownloadEnabled;
+
+ // automatic master download fetches only the inventory masters (items, stores, item->store mapping, Min Stock);
+ // other masters (users, complaints, villages) come with the manual Download Masters button
+ @Value("${sync.autoMasterDownload.inventoryOnly:true}")
+ private boolean masterDownloadInventoryOnly;
+
+ static final Set INVENTORY_MASTERS = Set.of("m_item", "m_itemcategory", "m_itemissueconfig", "m_uom",
+ "m_manufacturer", "m_pharmacologicalcategory", "m_supplier", "m_facilitytype", "m_facility",
+ "m_itemfacilitymapping");
+
+ private volatile long lastUploadAt;
+ private volatile String lastUploadResult;
+ private volatile long lastMasterDownloadAt;
+ private volatile String lastMasterDownloadResult;
+ private volatile long lastInventoryDownloadAt;
+ private volatile String lastInventoryDownloadResult;
+ private volatile String lastError;
+
+ // own thread: a long upload must not hold the shared scheduler thread
+ private final ExecutorService background = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "auto-data-sync");
+ t.setDaemon(true);
+ return t;
+ });
+
+ @PreDestroy
+ public void shutdown() {
+ background.shutdownNow();
+ }
+
+ @Scheduled(initialDelayString = "${sync.autoUpload.initialDelayMs:600000}", fixedDelayString = "${sync.autoUpload.intervalMs:1800000}")
+ public void scheduledUpload() {
+ if (uploadEnabled && centralAuthService.isEnabled()) {
+ background.submit(this::uploadOnce);
+ }
+ }
+
+ @Scheduled(initialDelayString = "${sync.autoMasterDownload.initialDelayMs:900000}", fixedDelayString = "${sync.autoMasterDownload.intervalMs:21600000}")
+ public void scheduledMasterDownload() {
+ if (masterDownloadEnabled && centralAuthService.isEnabled()) {
+ background.submit(this::masterDownloadOnce);
+ }
+ }
+
+ void uploadOnce() {
+ if (!SYNC_LOCK.tryLock()) {
+ return;
+ }
+ try {
+ String key = centralAuthService.getCentralSessionKey();
+ if (key == null) {
+ lastError = "No central connection - waiting for a user login";
+ return;
+ }
+ int vanID = centralAuthService.getVanID();
+ String user = centralAuthService.getConnectedUser();
+ String result = uploadDataToServerImpl.getDataToSyncToServer(vanID,
+ user != null && !user.isBlank() ? user : "auto-sync", key, null);
+ lastUploadAt = System.currentTimeMillis();
+ lastUploadResult = result;
+ lastError = null;
+ logger.info("Automatic upload for vanID {} finished: {}", vanID, result);
+ // internet is up: top up the BenID pool if it is low (same as the manual upload)
+ benIdAutoTopUpService.triggerNow();
+ if (inventoryDownloadEnabled)
+ inventoryDownload(vanID, key);
+ } catch (Exception e) {
+ lastError = "Upload: " + e.getMessage();
+ logger.warn("Automatic upload failed: {}", e.getMessage());
+ } finally {
+ SYNC_LOCK.unlock();
+ }
+ }
+
+ // Same as the "Inventory Data Download" button: indents, indent issues, stock transfers and stock entries
+ // that central has for this van's store. Its own failure does not undo the upload.
+ private void inventoryDownload(int vanID, String key) {
+ try {
+ int done = downloadDataFromServerTransactional.downloadTransactionalData(vanID, key, "datasync");
+ lastInventoryDownloadAt = System.currentTimeMillis();
+ lastInventoryDownloadResult = done > 0 ? "success" : "nothing downloaded";
+ logger.info("Automatic inventory download for vanID {} finished: {}", vanID, lastInventoryDownloadResult);
+ } catch (Exception e) {
+ lastError = "Inventory download: " + e.getMessage();
+ logger.warn("Automatic inventory download failed: {}", e.getMessage());
+ }
+ }
+
+ void masterDownloadOnce() {
+ if (!SYNC_LOCK.tryLock()) {
+ return;
+ }
+ try {
+ String key = centralAuthService.getCentralSessionKey();
+ if (key == null) {
+ lastError = "No central connection - waiting for a user login";
+ return;
+ }
+ int vanID = centralAuthService.getVanID();
+ Integer psm = dataSyncRepository.getVanProviderServiceMapID(vanID);
+ if (psm == null) {
+ lastError = "Master download: van " + vanID + " not found in m_van";
+ return;
+ }
+ String result = downloadDataFromServerImpl.downloadMasterDataFromServer(key, null, vanID, psm,
+ masterDownloadInventoryOnly ? INVENTORY_MASTERS : null);
+ if ("inProgress".equalsIgnoreCase(result)) {
+ logger.info("Master download already running (manual) - skipping the automatic one");
+ return;
+ }
+ lastMasterDownloadAt = System.currentTimeMillis();
+ lastMasterDownloadResult = result;
+ lastError = null;
+ logger.info("Automatic master download for vanID {} finished: {}", vanID, result);
+ } catch (Exception e) {
+ lastError = "Master download: " + e.getMessage();
+ logger.warn("Automatic master download failed: {}", e.getMessage());
+ } finally {
+ SYNC_LOCK.unlock();
+ }
+ }
+
+ /** For the Data Sync screen: last runs and the rows central has rejected. */
+ public JSONObject status() {
+ JSONObject s = new JSONObject();
+ s.put("enabled", centralAuthService.isEnabled());
+ s.put("uploadEnabled", uploadEnabled);
+ s.put("masterDownloadEnabled", masterDownloadEnabled);
+ s.put("lastUploadAt", lastUploadAt);
+ s.put("lastUploadResult", lastUploadResult == null ? JSONObject.NULL : lastUploadResult);
+ s.put("lastMasterDownloadAt", lastMasterDownloadAt);
+ s.put("lastMasterDownloadResult",
+ lastMasterDownloadResult == null ? JSONObject.NULL : lastMasterDownloadResult);
+ s.put("inventoryDownloadEnabled", inventoryDownloadEnabled);
+ s.put("lastInventoryDownloadAt", lastInventoryDownloadAt);
+ s.put("lastInventoryDownloadResult",
+ lastInventoryDownloadResult == null ? JSONObject.NULL : lastInventoryDownloadResult);
+ s.put("lastError", lastError == null ? JSONObject.NULL : lastError);
+ List> failures = dataSyncRepository.getSyncFailures();
+ long failedRows = failures.stream().mapToLong(f -> ((Number) f.get("failedRows")).longValue()).sum();
+ s.put("failedRows", failedRows);
+ s.put("failures", new JSONArray(failures));
+ return s;
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java
index 75a74842..99b4ec04 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java
@@ -23,9 +23,11 @@
import java.sql.Timestamp;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.sql.PreparedStatement;
@@ -40,6 +42,7 @@
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
+import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass;
import com.iemr.mmu.repo.syncActivity_syncLayer.SyncUtilityClassRepo;
import com.iemr.mmu.utils.validator.SqlIdentifierValidator;
@@ -127,9 +130,103 @@ public int updateProcessedFlagInVan(String schemaName, String tableName, List psm = getJdbcTemplate().queryForList(
+ "SELECT ProviderServiceMapID FROM db_iemr.m_van WHERE VanID = ? AND Deleted = false", Integer.class,
+ vanID);
+ return psm.isEmpty() ? null : psm.get(0);
+ }
+
+ /**
+ * Rows central rejected on upload (Processed = 'F'), per sync table and reason. They are sent again on every
+ * upload until central accepts them.
+ */
+ public List> getSyncFailures() {
+ jdbcTemplate = getJdbcTemplate();
+ List> result = new ArrayList<>();
+ Set seen = new HashSet<>();
+ for (SyncUtilityClass table : syncutilityClassRepo.findByDeletedOrderBySyncTableDetailID(false)) {
+ if (Boolean.TRUE.equals(table.getIsMaster()) || table.getSchemaName() == null
+ || table.getTableName() == null)
+ continue;
+ String qualified = SqlIdentifierValidator.validatedSchemaName(table.getSchemaName()) + "."
+ + SqlIdentifierValidator.validatedTableName(table.getTableName());
+ if (!seen.add(qualified.toLowerCase()))
+ continue;
+ try {
+ List> rows = jdbcTemplate.queryForList("SELECT '" + table.getTableName()
+ + "' AS tableName, SyncFailureReason AS reason, COUNT(*) AS failedRows, DATE_FORMAT(MAX(SyncedDate), '%d-%m-%Y %H:%i') AS lastTried "
+ + "FROM " + qualified + " WHERE Processed = 'F' GROUP BY SyncFailureReason");
+ result.addAll(rows);
+ } catch (Exception e) {
+ logger.warn("Sync failure check skipped for {}: {}", qualified, e.getMessage());
+ }
+ }
+ return result;
+ }
+
// ---------------------------------- End of Upload repository
// ---------------------------------- Download Repository
+
+ /**
+ * Down-sync copies the beneficiary tables but not i_ben_flow_outreach, and the tab download
+ * (FLW-API getBeneficiaryData) only serves beneficiaries that have a worklist row. So a
+ * beneficiary registered elsewhere and down-synced here never reached this laptop's tabs.
+ * Creates the missing worklist row for this van's beneficiaries, filled as TM-API does at
+ * registration. Processed = 'P' so it is not uploaded back: central already has its own row.
+ */
+ public int createMissingWorklistRows(int vanID, int providerServiceMapID) {
+ jdbcTemplate = getJdbcTemplate();
+
+ String village = "(SELECT h.%s FROM db_identity.i_beneficiarydetails_rmnch r "
+ + " JOIN db_identity.i_householddetails h ON h.houseoldId = r.houseoldId "
+ + " WHERE r.BeneficiaryRegID = bm.BenRegId LIMIT 1)";
+
+ String insert = "INSERT INTO db_iemr.i_ben_flow_outreach "
+ + " (beneficiary_reg_id, beneficiary_id, visit_no, ben_name, ben_age, ben_dob, ben_gender, "
+ + " ben_gender_val, ben_age_val, ben_phone_no, father_name, spouse_name, nurse_flag, doctor_flag, "
+ + " pharmacist_flag, doctor_signature_flag, BenArrivedFlag, created_by, created_date, modified_by, "
+ + " modified_date, registrationDate, districtID, district, villageID, village, "
+ + " providerServiceMapID, vanID, Processed, deleted) "
+ + " SELECT * FROM ( SELECT bm.BenRegId, rg.BeneficiaryID, 1 AS visit_no, "
+ + " TRIM(CONCAT_WS(' ', d.FirstName, NULLIF(d.MiddleName, ''), d.LastName)) AS ben_name, "
+ + " CONCAT(TIMESTAMPDIFF(YEAR, d.DOB, bm.CreatedDate), ' years - ', "
+ + " MOD(TIMESTAMPDIFF(MONTH, d.DOB, bm.CreatedDate), 12), ' months') AS ben_age, "
+ + " d.DOB, d.Gender, d.GenderId, TIMESTAMPDIFF(YEAR, d.DOB, bm.CreatedDate) AS ben_age_val, "
+ + " COALESCE(c.PreferredPhoneNum, c.PhoneNum1) AS phone, "
+ + " COALESCE(d.FatherName, '') AS father, COALESCE(d.SpouseName, '') AS spouse, "
+ + " 1 AS nf, 0 AS df, 0 AS pf, 0 AS dsf, 0 AS baf, bm.CreatedBy, bm.CreatedDate AS cdate, "
+ + " 'downsync' AS mby, NOW() AS mdate, bm.CreatedDate AS rdate, "
+ + " COALESCE(a.CurrDistrictId, a.PermDistrictId) AS districtID, "
+ + " COALESCE(a.CurrDistrict, a.PermDistrict) AS district, "
+ + " COALESCE(" + String.format(village, "villageid") + ", a.CurrVillageId, a.PermVillageId) AS villageID, "
+ + " COALESCE(" + String.format(village, "village") + ", a.CurrVillage, a.PermVillage) AS village, "
+ + " ? AS psm, bm.VanID, 'P' AS processed, 0 AS deleted "
+ + " FROM db_identity.i_beneficiarymapping bm "
+ + " JOIN db_identity.m_beneficiaryregidmapping rg ON rg.BenRegId = bm.BenRegId "
+ + " JOIN db_identity.i_beneficiarydetails d ON d.BeneficiaryDetailsId = bm.BenDetailsId "
+ + " LEFT JOIN db_identity.i_beneficiarycontacts c ON c.BenContactsID = bm.BenContactsId "
+ + " LEFT JOIN db_identity.i_beneficiaryaddress a ON a.BenAddressID = bm.BenAddressId "
+ + " WHERE bm.VanID = ? AND bm.Deleted = false "
+ // the van can hold beneficiaries of other service lines; only its own get a row
+ + " AND (SELECT r.ProviderServiceMapID FROM db_identity.i_beneficiarydetails_rmnch r "
+ + " WHERE r.BeneficiaryRegID = bm.BenRegId LIMIT 1) = ? "
+ + " AND NOT EXISTS (SELECT 1 FROM db_iemr.i_ben_flow_outreach f "
+ + " WHERE f.beneficiary_reg_id = bm.BenRegId) ) x "
+ + " WHERE x.villageID IS NOT NULL";
+
+ int inserted = jdbcTemplate.update(insert, providerServiceMapID, vanID, providerServiceMapID);
+
+ if (inserted > 0)
+ // same convention as TM-API: VanSerialNo is the row's own id
+ jdbcTemplate.update("UPDATE db_iemr.i_ben_flow_outreach SET VanSerialNo = ben_flow_id "
+ + " WHERE modified_by = 'downsync' AND VanSerialNo IS NULL");
+
+ return inserted;
+ }
+
public int[] updateLatestMasterInLocal(String query, List syncDataList) {
int[] i = null;
// get JDBC template
@@ -176,6 +273,17 @@ public String resolveLastModColumn(String schema, String table) {
return (found == null || found.isEmpty()) ? null : found.get(0);
}
+ public Set getTableColumns(String schema, String table) {
+ jdbcTemplate = getJdbcTemplate();
+
+ String query = " SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ";
+
+ Set columns = new HashSet<>();
+ for (String c : jdbcTemplate.queryForList(query, String.class, schema, table))
+ columns.add(c.toLowerCase());
+ return columns;
+ }
+
public List getDownSyncColumns(String schema, String table) {
jdbcTemplate = getJdbcTemplate();
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java
index c3c1e1d0..2c70854e 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java
@@ -163,6 +163,7 @@ public String startDownSync(String serverAuthorization, String jwtToken, Integer
}
progressCounter++;
}
+ createMissingWorklistRows(vanID, providerServiceMapID);
countOutstandingConflicts(downSyncTables, vanID);
} finally {
IN_PROGRESS.set(false);
@@ -207,6 +208,23 @@ public Map getDownSyncStatus() {
return resultMap;
}
+ /** Down-synced beneficiaries need a worklist row before this laptop's tabs can download them. */
+ private void createMissingWorklistRows(Integer vanID, Integer providerServiceMapID) {
+ try {
+ Integer psm = providerServiceMapID != null ? providerServiceMapID
+ : dataSyncRepository.getVanProviderServiceMapID(vanID);
+ if (psm == null) {
+ logger.warn("Worklist rows not created after down-sync: no service line for van {}", vanID);
+ return;
+ }
+ int created = dataSyncRepository.createMissingWorklistRows(vanID, psm);
+ logger.info("Created {} worklist rows for down-synced beneficiaries of van {}", created, vanID);
+ } catch (Exception e) {
+ // the down-sync itself already succeeded; a failure here must not undo it
+ logger.error("Creating worklist rows after down-sync failed for van " + vanID + " : " + e.getMessage(), e);
+ }
+ }
+
private void countOutstandingConflicts(List downSyncTables, Integer vanID) {
outstandingConflictCounter = 0;
outstandingConflictTables.clear();
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java
index 6a6d12bc..572724be 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java
@@ -27,9 +27,11 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
import org.slf4j.Logger;
@@ -45,6 +47,7 @@
import com.google.gson.Gson;
import com.iemr.mmu.data.syncActivity_syncLayer.MasterDownloadDataDigester;
+import com.iemr.mmu.service.benid.BenIdAutoTopUpService;
import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster;
import com.iemr.mmu.data.syncActivity_syncLayer.TempVan;
import com.iemr.mmu.repo.syncActivity_syncLayer.SyncDownloadMasterRepo;
@@ -60,6 +63,9 @@ public class DownloadDataFromServerImpl implements DownloadDataFromServer {
// rest URLs from server to download master data for van
@Value("${dataSyncDownloadUrl}")
private String dataSyncDownloadUrl;
+ // StopTB camp laptop (laptops are set up from copied databases); other MMU deployments keep standard behaviour
+ @Value("${stoptb.enforce.vanid:false}")
+ private boolean stopTBLaptop;
@Autowired
private SyncDownloadMasterRepo syncDownloadMasterRepo;
@Autowired
@@ -95,6 +101,14 @@ public class DownloadDataFromServerImpl implements DownloadDataFromServer {
*/
public String downloadMasterDataFromServer(String ServerAuthorization, String jwtToken, Integer vanID, Integer psmID)
throws Exception {
+ return downloadMasterDataFromServer(ServerAuthorization, jwtToken, vanID, psmID, null);
+ }
+
+ /**
+ * @param onlyTables master tables to download (case-insensitive), or null for all of m_downloadmastertable
+ */
+ public String downloadMasterDataFromServer(String ServerAuthorization, String jwtToken, Integer vanID, Integer psmID,
+ Set onlyTables) throws Exception {
if (totalCounter != progressCounter) {
return "inProgress";
@@ -103,6 +117,10 @@ public String downloadMasterDataFromServer(String ServerAuthorization, String jw
String successFlag = " Master download started ";
ArrayList downloadMasterList = syncDownloadMasterRepo.getDownloadTables();
+ if (onlyTables != null && downloadMasterList != null) {
+ downloadMasterList.removeIf(t -> t.getTableName() == null
+ || !onlyTables.contains(t.getTableName().toLowerCase()));
+ }
totalCounter = downloadMasterList.size();
progressCounter = 0;
@@ -178,6 +196,8 @@ public String call() {
private int downloadDataFromServer(SyncDownloadMaster syncDownloadMaster,String ServerAuthorization, String jwtToken)
throws Exception {
int successFlag = 0;
+ if (stopTBLaptop)
+ fullDownloadForLineAndVanMasters(syncDownloadMaster);
// initializing RestTemplate
RestTemplate restTemplate = new RestTemplate();
// Provide the required second argument, e.g., an empty string or appropriate authorization token
@@ -210,6 +230,18 @@ private int downloadDataFromServer(SyncDownloadMaster syncDownloadMaster,String
return successFlag;
}
+ /**
+ * StopTB laptops only. Service-line (P) and van (V) masters - hubs, vans, service points, stores, items, item-store mapping, roles -
+ * are always downloaded in full for this van's line: they are small, and a laptop set up from a copied database
+ * can carry download dates newer than central rows it never received (missing hub -> its vans fail on the
+ * foreign key). Rows are inserted/updated by ID, so repeating them is harmless. Masters shared by all lines (A,
+ * e.g. 661k villages) stay incremental.
+ */
+ static void fullDownloadForLineAndVanMasters(SyncDownloadMaster table) {
+ if ("P".equalsIgnoreCase(table.getMasterType()) || "V".equalsIgnoreCase(table.getMasterType()))
+ table.setLastDownloadDate(null);
+ }
+
private int updateMastersWithLatestData(MasterDownloadDataDigester masterDownloadDataDigester,
SyncDownloadMaster syncDownloadMaster) {
int successFlag = 0;
@@ -326,6 +358,10 @@ public Map getDownloadStatus() {
public int callCentralAPIToGenerateBenIDAndimportToLocal(String requestOBJ, String Authorization,
String ServerAuthorization, String token) throws Exception {
int i = 0, i1 = 0;
+ // never run together with the automatic top-up, so the same IDs are not requested twice
+ if (!BenIdAutoTopUpService.GENERATION_LOCK.tryLock(2, TimeUnit.MINUTES)) {
+ throw new Exception("BenID generation is already running, please try again in a few minutes");
+ }
try{
JSONObject originalRequest = new JSONObject(requestOBJ);
BigInteger vanID = null;
@@ -378,6 +414,8 @@ public int callCentralAPIToGenerateBenIDAndimportToLocal(String requestOBJ, Stri
} catch (Exception e) {
logger.error("Error while generating catch UNIQUE_ID at central server: " + e.getMessage());
throw new Exception("Error while generating catch UNIQUE_ID at central server: " + e.getMessage());
+ } finally {
+ BenIdAutoTopUpService.GENERATION_LOCK.unlock();
}
return i;
}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java
index ee3c39a5..e441ea6e 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java
@@ -227,8 +227,6 @@ public int downloadTransactionalData(int vanID, String ServerAuthorization, Stri
default:
}
-
- i++;
}
return 1;
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java
index d9567861..b0aa115d 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java
@@ -26,6 +26,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
@@ -80,6 +81,11 @@ public class UploadDataToServerImpl implements UploadDataToServer {
@Value("${BATCH_SIZE}")
private int BATCH_SIZE;
+ // StopTB camp laptop: a table configured in several sync groups is uploaded only by its last group (see
+ // skipTablesUploadedByLaterGroup)
+ @Value("${stoptb.enforce.vanid:false}")
+ private boolean stopTBLaptop;
+
@Autowired
private DataSyncRepository dataSyncRepository;
@Autowired
@@ -151,9 +157,33 @@ private String startDataSync(int vanID, String user, String Authorization, Strin
String tableKey = obj.getSchemaName() + "." + obj.getTableName();
boolean tableHasError = false;
- // get data from DB to sync to server
- List> syncData = getDataToSync(obj.getSchemaName(), obj.getTableName(),
- obj.getVanColumnName());
+ String[] columns = { obj.getVanColumnName(), obj.getServerColumnName() };
+ List> syncData;
+ try {
+ if (stopTBLaptop)
+ columns = useRenamedColumns(obj.getTableName(), columns[0], columns[1],
+ dataSyncRepository.getTableColumns(obj.getSchemaName(), obj.getTableName()));
+
+ // get data from DB to sync to server
+ syncData = getDataToSync(obj.getSchemaName(), obj.getTableName(), columns[0]);
+ } catch (Exception e) {
+ if (!stopTBLaptop)
+ throw e;
+ // StopTB camp laptop: one table that cannot be read must not stop the upload of the others;
+ // its rows stay unprocessed and go on the next upload
+ logger.error("Upload of table {} skipped: {}", tableKey, e.getMessage());
+ groupHasFailures = true;
+ Map tableDetails = new HashMap<>();
+ tableDetails.put("tableName", obj.getTableName());
+ tableDetails.put("schemaName", obj.getSchemaName());
+ tableDetails.put("status", "failed");
+ tableDetails.put("totalRecords", 0);
+ tableDetails.put("successfulRecords", 0);
+ tableDetails.put("failedRecords", 0);
+ tableDetails.put("failureReasons", List.of("Table could not be read: " + e.getMessage()));
+ tableDetailsList.add(tableDetails);
+ continue;
+ }
logger.info("Fetched syncData for schema {} and table {}: {}", obj.getSchemaName(), obj.getTableName(),
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(syncData));
@@ -179,7 +209,7 @@ private String startDataSync(int vanID, String user, String Authorization, Strin
Map syncResult = syncDataToServer(vanID, obj.getSchemaName(),
obj.getTableName(),
- obj.getVanAutoIncColumnName(), obj.getServerColumnName(), syncDataBatch, user,
+ obj.getVanAutoIncColumnName(), columns[1], syncDataBatch, user,
Authorization, token);
if (syncResult == null) {
@@ -221,7 +251,7 @@ private String startDataSync(int vanID, String user, String Authorization, Strin
Map syncResult = syncDataToServer(vanID, obj.getSchemaName(),
obj.getTableName(),
- obj.getVanAutoIncColumnName(), obj.getServerColumnName(), syncDataBatch, user,
+ obj.getVanAutoIncColumnName(), columns[1], syncDataBatch, user,
Authorization, token);
if (syncResult == null) {
@@ -407,10 +437,59 @@ private String startDataSync(int vanID, String user, String Authorization, Strin
private List getVanAndServerColumns(Integer groupID) throws Exception {
List syncUtilityClassList = getVanAndServerColumnList(groupID);
+ if (stopTBLaptop)
+ syncUtilityClassList = skipTablesUploadedByLaterGroup(syncUtilityClassList, groupID,
+ syncutilityClassRepo.findByDeletedOrderBySyncTableDetailID(false));
logger.debug("Fetched SyncUtilityClass list for groupID {}: {}", groupID, syncUtilityClassList);
return syncUtilityClassList;
}
+ /**
+ * Inventory tables are configured twice: group 5 (old lists without the store columns of stock-outs and patient
+ * issues, batch insert fails) and group 9 "Inventory" (complete). Group 5 ran first and marked the rows uploaded,
+ * so central got stock-outs without their camp store. A table that a later group also uploads is skipped here.
+ */
+ static List skipTablesUploadedByLaterGroup(List groupTables, int groupID,
+ List allTables) {
+ Map lastGroup = new HashMap<>();
+ for (SyncUtilityClass t : allTables) {
+ if (t.getTableName() != null && t.getSyncTableGroupID() != null)
+ lastGroup.merge(t.getTableName().toLowerCase(), t.getSyncTableGroupID(), Math::max);
+ }
+ List kept = new ArrayList<>();
+ for (SyncUtilityClass t : groupTables) {
+ Integer last = t.getTableName() == null ? null : lastGroup.get(t.getTableName().toLowerCase());
+ if (last == null || last <= groupID)
+ kept.add(t);
+ }
+ return kept;
+ }
+
+ // columns renamed by DB scripts after the sync table lists were written (tb_diagnostic_order: V114 renamed
+ // reason_for_refusal to reason_to_close, V117 reverted it, V125 renamed it again)
+ private static final Map> RENAMED_COLUMNS = Map.of("tb_diagnostic_order",
+ Map.of("reason_for_refusal", "reason_to_close", "reason_to_close", "reason_for_refusal"));
+
+ /**
+ * The sync table list may still name a column by its old name (laptop m_synctabledetail has reason_for_refusal,
+ * the table has reason_to_close) and the whole upload stopped with "bad SQL grammar". The name the table really
+ * has is used in both the van and the server column lists (central has the same DB version).
+ */
+ static String[] useRenamedColumns(String tableName, String vanColumns, String serverColumns,
+ Set tableColumns) {
+ Map renamed = tableName == null ? null : RENAMED_COLUMNS.get(tableName.toLowerCase());
+ if (renamed == null || tableColumns == null || tableColumns.isEmpty())
+ return new String[] { vanColumns, serverColumns };
+ for (Map.Entry r : renamed.entrySet()) {
+ if (!tableColumns.contains(r.getKey()) && tableColumns.contains(r.getValue())) {
+ String listed = "(?i)\\b" + r.getKey() + "\\b";
+ vanColumns = vanColumns == null ? null : vanColumns.replaceAll(listed, r.getValue());
+ serverColumns = serverColumns == null ? null : serverColumns.replaceAll(listed, r.getValue());
+ }
+ }
+ return new String[] { vanColumns, serverColumns };
+ }
+
public List getVanAndServerColumnList(Integer groupID) throws Exception {
List syncUtilityClassList = syncutilityClassRepo
.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(groupID, false);
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java
index 0da5bf4b..75aa52a4 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java
@@ -173,6 +173,22 @@ public int checkRecordIsAlreadyPresentOrNot(String schemaName, String tableName,
}
}
+ /**
+ * Central ID of a row a van uploaded earlier, found by the van's own ID (VanSerialNo) and the van; null if that
+ * row is not on central (yet).
+ */
+ public Long findCentralIdOfVanRow(String schemaName, String tableName, String idColumn, Object vanSerialNo,
+ String vanID) {
+ if (!isValidSchemaName(schemaName) || !isValidDatabaseIdentifierCharacter(tableName)
+ || !isValidDatabaseIdentifierCharacter(idColumn)) {
+ throw new IllegalArgumentException("Invalid identifiers provided.");
+ }
+ List ids = getJdbcTemplate().queryForList("SELECT " + idColumn + " FROM " + schemaName + "." + tableName
+ + " WHERE VanSerialNo = ? AND VanID = ? ORDER BY " + idColumn + " DESC LIMIT 1", Long.class,
+ String.valueOf(vanSerialNo), vanID);
+ return ids.isEmpty() ? null : ids.get(0);
+ }
+
public int[] syncDataToCentralDB(String schema, String tableName, String serverColumns, String query,
List syncDataList) {
jdbcTemplate = getJdbcTemplate();
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java
index c76962b4..50c49c0e 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java
@@ -205,7 +205,8 @@ else if (masterType.equalsIgnoreCase("P")) {
}
logger.info("Select query central: " + baseQuery);
logger.info("Last Downloaded Date " + lastDownloadDate);
- logger.info("Result set Details: " + resultSetList);
+ // Row count only: logging the rows of a large master (villages, 661k rows) exhausted the heap.
+ logger.info("Rows returned: " + resultSetList.size());
return resultSetList;
}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java
new file mode 100644
index 00000000..a03526f9
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java
@@ -0,0 +1,62 @@
+package com.iemr.mmu.service.dataSyncLayerCentral;
+
+import java.time.Duration;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+
+/***
+ * @purpose Hands back a short-lived presigned URL for the latest successfully-pushed
+ * diagnostic document matching a beneficiary+documentType, generated on demand from
+ * the object's S3 key (s3_path) - the bucket is private, so no permanent URL is
+ * ever persisted or handed out.
+ */
+@Service
+public class DiagnosticDocumentFetchService {
+
+ private static final Duration URL_VALIDITY = Duration.ofMinutes(15);
+
+ @Value("${diagnostic.documents.s3.bucket}")
+ private String bucket;
+
+ @Autowired
+ private DiagnosticDocumentRepository diagnosticDocumentRepository;
+
+ @Autowired
+ private S3Presigner s3Presigner;
+
+ /***
+ * @return null if no successfully-pushed document matches, otherwise the download details
+ * (documentType, orderType, externalOrderId, contentType, lastModDate, downloadUrl,
+ * urlExpiresInSeconds)
+ */
+ public Map getLatestDocumentDownload(Long beneficiaryId, String documentType) {
+ Map row = diagnosticDocumentRepository.findLatestDocument(beneficiaryId, documentType);
+ if (row == null) {
+ return null;
+ }
+
+ String s3Key = (String) row.get("s3_path");
+ GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(bucket).key(s3Key).build();
+ GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder().signatureDuration(URL_VALIDITY)
+ .getObjectRequest(getObjectRequest).build();
+ String downloadUrl = s3Presigner.presignGetObject(presignRequest).url().toString();
+
+ Map result = new java.util.HashMap<>();
+ result.put("externalOrderId", row.get("external_order_id"));
+ result.put("orderType", row.get("order_type"));
+ result.put("documentType", row.get("document_type"));
+ result.put("contentType", row.get("content_type"));
+ result.put("originalFileName", row.get("original_file_name"));
+ result.put("lastModDate", String.valueOf(row.get("last_mod_date")));
+ result.put("downloadUrl", downloadUrl);
+ result.put("urlExpiresInSeconds", URL_VALIDITY.getSeconds());
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java
new file mode 100644
index 00000000..d04d841f
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java
@@ -0,0 +1,139 @@
+package com.iemr.mmu.service.dataSyncLayerCentral;
+
+import java.lang.reflect.Type;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.ServerSideEncryption;
+
+/***
+ * @purpose Receives a batch of decrypted diagnostic documents pushed from a van and stores
+ * each in S3 - purely a storage relay, no database writes here. The pushing van
+ * persists its own record locally (DiagnosticDocumentPushServiceImpl.markPushedToCentral,
+ * keyed off the s3Path this returns in each ack) - the central server's own database is
+ * left untouched.
+ */
+@Service
+public class DiagnosticDocumentIngestService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+ private static final Gson GSON = new Gson();
+ private static final String ENCRYPTED_FILE_SUFFIX = ".enc";
+
+ @Value("${diagnostic.documents.s3.bucket}")
+ private String bucket;
+
+ @Autowired
+ private S3Client s3Client;
+
+ public String ingestDocuments(String requestOBJ) throws Exception {
+ Type listType = new TypeToken>>() {
+ }.getType();
+ List> items = GSON.fromJson(requestOBJ, listType);
+ if (items == null || items.isEmpty()) {
+ return GSON.toJson(new ArrayList<>());
+ }
+
+ List> acks = new ArrayList<>();
+ for (Map item : items) {
+ acks.add(ingestOne(item));
+ }
+ return GSON.toJson(acks);
+ }
+
+ private Map ingestOne(Map item) {
+ Map ack = new HashMap<>();
+ ack.put("documentId", item.get("documentId"));
+ ack.put("diagnosticOrderId", item.get("diagnosticOrderId"));
+ ack.put("externalOrderId", item.get("externalOrderId"));
+ ack.put("documentType", item.get("documentType"));
+
+ try {
+ ack.put("documentId", asLong(item.get("documentId")));
+ Long diagnosticOrderId = asLong(item.get("diagnosticOrderId"));
+ ack.put("diagnosticOrderId", diagnosticOrderId);
+ String documentType = (String) item.get("documentType");
+
+ byte[] plaintext = Base64.getDecoder().decode((String) item.get("fileContentBase64"));
+
+ String sha256Hash = (String) item.get("sha256Hash");
+ if (sha256Hash != null && !sha256Hash.equalsIgnoreCase(sha256Hex(plaintext))) {
+ ack.put("status", "FAILED");
+ ack.put("error", "sha256 mismatch on receipt");
+ return ack;
+ }
+
+ Long beneficiaryId = asLong(item.get("beneficiaryId"));
+ Long vanId = asLong(item.get("vanID"));
+ String orderType = (String) item.get("orderType");
+ String storedFileName = (String) item.get("storedFileName");
+ if (vanId == null || beneficiaryId == null || orderType == null || documentType == null
+ || storedFileName == null) {
+ ack.put("status", "FAILED");
+ ack.put("error", "Missing vanID, beneficiaryId, orderType, documentType or storedFileName");
+ return ack;
+ }
+ String s3Key = vanId + "/" + beneficiaryId + "/" + orderType + "/" + documentType + "/"
+ + objectFileName(storedFileName, (String) item.get("fileExtension"));
+ String contentType = (String) item.get("contentType");
+
+ s3Client.putObject(
+ PutObjectRequest.builder().bucket(bucket).key(s3Key)
+ .contentType(contentType != null ? contentType : "application/octet-stream")
+ .serverSideEncryption(ServerSideEncryption.AES256).build(),
+ RequestBody.fromBytes(plaintext));
+
+ ack.put("status", "SUCCESS");
+ ack.put("s3Path", s3Key);
+ } catch (Exception e) {
+ logger.error("Error ingesting diagnostic document: documentId=" + item.get("documentId")
+ + ", diagnosticOrderId=" + item.get("diagnosticOrderId")
+ + ", documentType=" + item.get("documentType"), e);
+ ack.put("status", "FAILED");
+ ack.put("error", e.getMessage());
+ }
+ return ack;
+ }
+
+ /***
+ * @purpose The van stores files encrypted as ".enc", but S3 holds the decrypted
+ * content - so the object is named with the real extension the van derived from
+ * its content type (e.g. CAD.enc -> CAD.pdf), not the misleading ".enc".
+ */
+ private static String objectFileName(String storedFileName, String fileExtension) {
+ String baseName = storedFileName.endsWith(ENCRYPTED_FILE_SUFFIX)
+ ? storedFileName.substring(0, storedFileName.length() - ENCRYPTED_FILE_SUFFIX.length())
+ : storedFileName;
+ return fileExtension != null && !fileExtension.isEmpty() ? baseName + "." + fileExtension : baseName;
+ }
+
+ private static Long asLong(Object value) {
+ return value == null ? null : ((Number) value).longValue();
+ }
+
+ private static String sha256Hex(byte[] data) throws Exception {
+ byte[] digest = MessageDigest.getInstance("SHA-256").digest(data);
+ StringBuilder hex = new StringBuilder(digest.length * 2);
+ for (byte b : digest) {
+ hex.append(String.format(Locale.ROOT, "%02x", b));
+ }
+ return hex.toString();
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java
new file mode 100644
index 00000000..cc817200
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java
@@ -0,0 +1,274 @@
+package com.iemr.mmu.service.dataSyncLayerCentral;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.reflect.TypeToken;
+import com.iemr.mmu.utils.CryptoUtil;
+import com.iemr.mmu.utils.RestTemplateUtil;
+
+import java.lang.reflect.Type;
+
+/***
+ * @purpose Reads MMU-API's own locally-pending diagnostic documents (docsProcessed='N',
+ * shared db_iemr.tb_diagnostic_document table), decrypts the file off the shared
+ * filesystem, and pushes each batch to the further central server's
+ * /dataSync/diagnostic-documents endpoint - the same relay shape as
+ * UploadDataToServerImpl's push to dataSyncUploadUrl, just for this table instead
+ * of the generic sync-group config. MMU-API's own /dataSync/diagnostic-documents
+ * ingest endpoint (DiagnosticDocumentCentralIngestService) is untouched by this -
+ * this service is a separate outbound relay, not a caller of it.
+ */
+@Service
+public class DiagnosticDocumentPushServiceImpl {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
+
+ private static final Map CONTENT_TYPE_EXTENSIONS = new HashMap<>();
+ static {
+ CONTENT_TYPE_EXTENSIONS.put("application/pdf", "pdf");
+ CONTENT_TYPE_EXTENSIONS.put("image/jpeg", "jpg");
+ CONTENT_TYPE_EXTENSIONS.put("image/png", "png");
+ }
+
+ @Value("${diagnostic.documents.storage-root}")
+ private String storageRoot;
+
+ @Value("${diagnosticDocumentUploadUrl}")
+ private String diagnosticDocumentUploadUrl;
+
+ @Value("${diagnosticDocument.push.batchSize:3}")
+ private int batchSize;
+
+ @Autowired
+ private DiagnosticDocumentRepository diagnosticDocumentRepository;
+
+ @Autowired
+ private CryptoUtil cryptoUtil;
+
+ public String pushPendingDocuments(String Authorization) throws Exception {
+ List> pendingRows = diagnosticDocumentRepository.findPendingDocuments();
+ int totalSucceeded = 0;
+ List failureReasons = new ArrayList<>();
+
+ for (int offset = 0; offset < pendingRows.size(); offset += batchSize) {
+ List> rows = pendingRows.subList(offset, Math.min(offset + batchSize, pendingRows.size()));
+
+ List> payloadItems = new ArrayList<>();
+ Map> rowsById = new HashMap<>();
+ for (Map row : rows) {
+ Long rowId = asLong(row.get("id"));
+ String base64Plaintext;
+ try {
+ String storedPath = (String) row.get("stored_path");
+ Path filePath = Paths.get(storageRoot, storedPath);
+ String encryptedPayload = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
+ base64Plaintext = cryptoUtil.decrypt(encryptedPayload);
+ } catch (Exception e) {
+ logger.warn("Skipping diagnostic document push, could not read file off disk: id={}, error={}",
+ rowId, e.getMessage());
+ markFailed(rowId, "Could not read file off shared filesystem: " + e.getMessage(), failureReasons);
+ continue;
+ }
+ if (base64Plaintext == null) {
+ logger.warn("Skipping diagnostic document push, decrypt failed: id={}", rowId);
+ markFailed(rowId, "Decrypt failed", failureReasons);
+ continue;
+ }
+
+ Long diagnosticOrderId = asLong(row.get("diagnostic_order_id"));
+ String externalOrderId = (String) row.get("external_order_id");
+ String documentType = (String) row.get("document_type");
+ String contentType = (String) row.get("content_type");
+
+ Map item = new HashMap<>();
+ item.put("documentId", rowId);
+ item.put("diagnosticOrderId", diagnosticOrderId);
+ item.put("externalOrderId", externalOrderId);
+ item.put("beneficiaryId", row.get("beneficiary_id"));
+ item.put("orderType", row.get("order_type"));
+ item.put("documentType", documentType);
+ item.put("storedFileName", row.get("stored_file_name"));
+ item.put("sha256Hash", row.get("sha256_hash"));
+ item.put("contentType", contentType);
+ item.put("fileExtension", extensionFor(contentType));
+ item.put("originalFileName", row.get("original_file_name"));
+ item.put("vanID", row.get("vanID"));
+ item.put("parkingPlaceID", row.get("parkingPlaceID"));
+ item.put("vanSerialNo", row.get("vanSerialNo"));
+ item.put("fileContentBase64", base64Plaintext);
+ payloadItems.add(item);
+ rowsById.put(rowId, row);
+ }
+
+ if (payloadItems.isEmpty()) {
+ continue;
+ }
+
+ String requestOBJ = new Gson().toJson(payloadItems);
+ List> acks;
+ try {
+ HttpEntity request = RestTemplateUtil.createRequestEntity(requestOBJ, Authorization, "datasync");
+ RestTemplate restTemplate = new RestTemplate();
+ ResponseEntity response = restTemplate.exchange(diagnosticDocumentUploadUrl, HttpMethod.POST,
+ request, String.class);
+
+ if (response == null || !response.hasBody()) {
+ logger.warn("No response body from central server for diagnostic document push, marking batch failed");
+ markBatchFailed(rowsById, "No response body from central server", failureReasons);
+ continue;
+ }
+
+ // Central server wraps every response in the shared OutputResponse envelope
+ // ({"data": [...], "statusCode":200, ...}) - the ack array lives under "data".
+ JsonElement parsedBody = JsonParser.parseString(response.getBody());
+ if (!parsedBody.isJsonObject()) {
+ logger.warn("Unexpected response shape from central server for diagnostic document push, marking batch failed");
+ markBatchFailed(rowsById, "Unexpected response shape from central server", failureReasons);
+ continue;
+ }
+ JsonObject envelope = parsedBody.getAsJsonObject();
+ if (!envelope.has("statusCode") || envelope.get("statusCode").getAsInt() != 200
+ || !envelope.has("data")) {
+ logger.warn("Central server reported failure for diagnostic document push batch, marking batch failed: {}",
+ response.getBody());
+ markBatchFailed(rowsById, "Central server reported failure: " + response.getBody(), failureReasons);
+ continue;
+ }
+
+ Type ackListType = new TypeToken>>() {
+ }.getType();
+ acks = new Gson().fromJson(envelope.get("data"), ackListType);
+ if (acks == null) {
+ markBatchFailed(rowsById, "Central server returned no acknowledgements", failureReasons);
+ continue;
+ }
+ } catch (Exception e) {
+ logger.error("Error calling central server for diagnostic document push, marking batch failed", e);
+ markBatchFailed(rowsById, "Error calling central server: " + e.getMessage(), failureReasons);
+ continue;
+ }
+
+ int batchSuccessCount = 0;
+ Map> unmatchedRowsById = new HashMap<>(rowsById);
+ for (Map ack : acks) {
+ Long rowId = ack.get("documentId") instanceof Number ? asLong(ack.get("documentId")) : null;
+ if (rowId == null || unmatchedRowsById.remove(rowId) == null) {
+ continue;
+ }
+ if ("SUCCESS".equalsIgnoreCase((String) ack.get("status"))) {
+ diagnosticDocumentRepository.markPushedToCentral(rowId, (String) ack.get("s3Path"));
+ batchSuccessCount++;
+ } else {
+ String reason = (String) ack.get("error");
+ logger.warn("Central server rejected diagnostic document push: id={}, externalOrderId={}, documentType={}, error={}",
+ rowId, ack.get("externalOrderId"), ack.get("documentType"), reason);
+ markFailed(rowId, reason != null ? reason : "Central server rejected the document", failureReasons);
+ }
+ }
+ if (!unmatchedRowsById.isEmpty()) {
+ // Central sent back fewer acks than documents we sent - whatever wasn't
+ // accounted for must not be silently left at its previous status forever.
+ logger.warn(
+ "Diagnostic document push: {} row(s) in this batch got no matching ack back, marking failed",
+ unmatchedRowsById.size());
+ markBatchFailed(unmatchedRowsById, "No acknowledgement received from central server", failureReasons);
+ }
+
+ totalSucceeded += batchSuccessCount;
+ logger.info("Diagnostic document push batch complete: attempted={}, succeeded={}", payloadItems.size(),
+ batchSuccessCount);
+ }
+
+ if (pendingRows.isEmpty()) {
+ return "No data to sync";
+ }
+
+ // Same table-level summary shape as /van-to-server (UploadDataToServerImpl) so the
+ // data sync screen can read both the same way.
+ int totalRecords = pendingRows.size();
+ int failedRecords = failureReasons.size();
+ String status;
+ if (failedRecords == 0) {
+ status = "success";
+ } else if (totalSucceeded == 0) {
+ status = "failed";
+ } else {
+ status = "partial";
+ }
+
+ Map finalResponse = new LinkedHashMap<>();
+ finalResponse.put("response",
+ "success".equals(status) ? "Data sync completed successfully" : "Data sync completed with failures");
+ finalResponse.put("schemaName", "db_iemr");
+ finalResponse.put("tableName", "tb_diagnostic_document");
+ finalResponse.put("status", status);
+ finalResponse.put("totalRecords", totalRecords);
+ finalResponse.put("successfulRecords", totalSucceeded);
+ finalResponse.put("failedRecords", failedRecords);
+ if (!failureReasons.isEmpty()) {
+ finalResponse.put("failureReasons", failureReasons);
+ }
+
+ logger.info("Diagnostic document push complete: {} (Success: {}, Failed: {}, Total: {})", status,
+ totalSucceeded, failedRecords, totalRecords);
+ return new Gson().toJson(finalResponse);
+ }
+
+ /***
+ * @purpose Marks one row failed locally and records its reason for the run summary.
+ */
+ private void markFailed(Long rowId, String reason, List failureReasons) {
+ diagnosticDocumentRepository.markPushFailed(rowId, reason);
+ failureReasons.add("documentId " + rowId + ": " + reason);
+ }
+
+ /***
+ * @purpose Called when the whole batch call to the central server fails (no/garbled
+ * response, non-200, or a thrown exception) - marks every row that was in this
+ * batch as failed, matching how /van-to-server marks a whole batch failed on a
+ * connection error, rather than leaving them stuck at docsProcessed='N' forever.
+ */
+ private void markBatchFailed(Map> rowsById, String reason, List failureReasons) {
+ for (Long rowId : rowsById.keySet()) {
+ markFailed(rowId, reason, failureReasons);
+ }
+ }
+
+ private static Long asLong(Object value) {
+ return value == null ? null : ((Number) value).longValue();
+ }
+
+ private static String extensionFor(String contentType) {
+ if (contentType == null) {
+ return "bin";
+ }
+ String extension = CONTENT_TYPE_EXTENSIONS.get(contentType.toLowerCase());
+ if (extension != null) {
+ return extension;
+ }
+ int slashIndex = contentType.indexOf('/');
+ return slashIndex >= 0 && slashIndex < contentType.length() - 1 ? contentType.substring(slashIndex + 1) : "bin";
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java
new file mode 100644
index 00000000..53c30c5f
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java
@@ -0,0 +1,95 @@
+package com.iemr.mmu.service.dataSyncLayerCentral;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+/***
+ * @purpose Local (van) side persistence for the diagnostic-document push pipeline - the central
+ * server is a pure S3 storage relay (DiagnosticDocumentCentralIngestService) and writes
+ * nothing to its own database, so this repository only ever operates on the pushing
+ * van's own local db_iemr.tb_diagnostic_document rows.
+ */
+@Service
+public class DiagnosticDocumentRepository {
+
+ @Autowired
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ private JdbcTemplate getJdbcTemplate() {
+ if (this.jdbcTemplate == null) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+ return this.jdbcTemplate;
+ }
+
+ /***
+ * @purpose Latest successfully-pushed document for a beneficiary+documentType, used to hand
+ * back a fresh presigned URL on demand instead of persisting a permanent (and, for a
+ * private bucket, non-functional) URL. s3_path holds the S3 key for a
+ * docsProcessed='P' row (markPushedToCentral() below writes it there).
+ */
+ public Map findLatestDocument(Long beneficiaryId, String documentType) {
+ String query = "SELECT id, external_order_id, order_type, document_type, s3_path, content_type, "
+ + "original_file_name, last_mod_date FROM db_iemr.tb_diagnostic_document "
+ + "WHERE beneficiary_id = ? AND document_type = ? AND docsProcessed = 'P' "
+ + "ORDER BY last_mod_date DESC, id DESC LIMIT 1";
+ List> rows = getJdbcTemplate().queryForList(query, beneficiaryId, documentType);
+ return rows.isEmpty() ? null : rows.get(0);
+ }
+
+ /***
+ * @purpose Snapshot of every locally-pending row's metadata (docsProcessed='N' or 'F') at
+ * the start of a push run, taken in one query so a row this same run marks 'F'
+ * partway through (e.g. an auth failure) is never re-picked-up later in the same
+ * run - it only becomes eligible again on the next trigger, alongside whatever's
+ * newly 'N' by then. Only metadata is loaded here (id, hashes, filenames, etc.) -
+ * each row's actual file content is still read off disk and decrypted one batch at
+ * a time by the caller, so the whole backlog's decrypted content is never held in
+ * memory at once.
+ */
+ public List> findPendingDocuments() {
+ String query = "SELECT id, diagnostic_order_id, external_order_id, beneficiary_id, order_type, document_type, "
+ + "stored_file_name, stored_path, sha256_hash, content_type, original_file_name, "
+ + "vanID, parkingPlaceID, vanSerialNo FROM db_iemr.tb_diagnostic_document "
+ + "WHERE docsProcessed = 'N' OR docsProcessed = 'F' ORDER BY id ASC";
+ return getJdbcTemplate().queryForList(query);
+ }
+
+ /***
+ * @purpose Marks a document as successfully pushed on the LOCAL (van) row - the central
+ * server itself writes nothing to its own database. s3Path is the S3 key the
+ * central server's ack reported back, persisted here so the van's own local DB
+ * knows where the document ended up, not just that it did.
+ */
+ public void markPushedToCentral(Long id, String s3Path) {
+ String update = "UPDATE db_iemr.tb_diagnostic_document SET processed = 'N', docsProcessed = 'P', "
+ + "s3_path = ?, docSyncedDate = NOW(), docSyncFailureReason = NULL, last_mod_date = NOW() WHERE id = ?";
+ getJdbcTemplate().update(update, s3Path, id);
+ }
+
+ /***
+ * @purpose Marks a document push to the further central server as failed - docsProcessed='F',
+ * matching the P/F status convention used by the generic sync pipeline's
+ * DataSyncRepository.updateProcessedFlagInVan for /van-to-server. Persists why in
+ * docSyncFailureReason (mirroring the generic pipeline's own SyncFailureReason, kept
+ * separate since this pipeline's docsProcessed/docSyncedDate are their own dedicated
+ * columns) so a failure isn't only visible in the application log. Also clears
+ * s3_path - otherwise a row that succeeded once, then got re-attempted and failed,
+ * would keep showing a stale S3 key while docsProcessed says 'F'. docSyncedDate is
+ * left untouched - it records the last time this row was actually confirmed synced,
+ * if ever.
+ */
+ public void markPushFailed(Long id, String reason) {
+ String update = "UPDATE db_iemr.tb_diagnostic_document SET processed = 'N', docsProcessed = 'F', "
+ + "s3_path = NULL, docSyncFailureReason = ?, last_mod_date = NOW() WHERE id = ?";
+ getJdbcTemplate().update(update, reason, id);
+ }
+}
diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java
index c0e508cd..4db30fff 100644
--- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java
@@ -25,6 +25,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -53,6 +54,21 @@ public class GetDataFromVanAndSyncToDBImpl implements GetDataFromVanAndSyncToDB
@Value("${downSync.markDeliveredOnUpSync:true}")
private boolean markDownSyncedOnUpSync;
+ // fill VanSerialNo / SyncFacilityID from the van row when not sent by name (see fillVanKeys)
+ @Value("${dataSync.fillVanKeys:true}")
+ private boolean fillVanKeysEnabled = true;
+
+ // replace van IDs in link columns by central IDs (see translateVanLinks); off by default, on for StopTB central
+ @Value("${dataSync.translateVanLinks:false}")
+ private boolean translateVanLinksEnabled;
+
+ // link column -> {table it points to, that table's ID column}
+ private static final Map VAN_LINKS = new LinkedHashMap<>();
+ static {
+ VAN_LINKS.put("BenVisitID", new String[] { "t_benvisitdetail", "BenVisitID" });
+ VAN_LINKS.put("PrescriptionID", new String[] { "t_prescription", "PrescriptionID" });
+ }
+
private static final Map> TABLE_GROUPS = new HashMap<>();
static {
TABLE_GROUPS.put(1,
@@ -372,7 +388,21 @@ private boolean performGenericTableSync(SyncUploadDataDigester syncUploadDataDig
// CRITICAL FIX: Keep null as null, don't convert to string
cleanRecord.put(cleanKey.trim(), value);
}
-
+ if (fillVanKeysEnabled)
+ fillVanKeys(cleanRecord, serverColumnsList, vanAutoIncColumnName);
+ if (translateVanLinksEnabled) {
+ Object linkVanID = cleanRecord.get("VanID") != null ? cleanRecord.get("VanID") : cleanRecord.get("vanID");
+ String linkError = translateVanLinks(schemaName, syncTableName, cleanRecord, serverColumnsList,
+ linkVanID == null ? null : String.valueOf(linkVanID));
+ if (linkError != null) {
+ syncResults.add(new SyncResult(schemaName, syncTableName,
+ String.valueOf(cleanRecord.get(vanAutoIncColumnName)), syncUploadDataDigester.getSyncedBy(),
+ false, linkError));
+ overallSuccess = false;
+ continue;
+ }
+ }
+
String vanSerialNo = String.valueOf(cleanRecord.get(vanAutoIncColumnName));
// Column may be "vanID" (Stop TB tables) or "VanID" (standard tables) — check both
@@ -606,6 +636,57 @@ private boolean performGenericTableSync(SyncUploadDataDigester syncUploadDataDig
return overallSuccess;
}
+ /**
+ * m_synctabledetail maps van columns to server columns by position: the van's own ID (VanAutoIncColumnName,
+ * e.g. BenVisitID, ItemStockEntryID) sits where the server list has VanSerialNo. Records are filled by name, so
+ * without this VanSerialNo stays NULL: the row cannot be matched on the next upload (duplicates) and joins on
+ * VanSerialNo break. Same for SyncFacilityID, which some van lists do not send: it is the row's store.
+ */
+ static void fillVanKeys(Map record, List serverColumns, String vanAutoIncColumnName) {
+ if (hasColumn(serverColumns, "VanSerialNo") && record.get("VanSerialNo") == null
+ && vanAutoIncColumnName != null && record.get(vanAutoIncColumnName) != null) {
+ record.put("VanSerialNo", record.get(vanAutoIncColumnName));
+ }
+ if (hasColumn(serverColumns, "SyncFacilityID") && record.get("SyncFacilityID") == null
+ && record.get("FacilityID") != null) {
+ record.put("SyncFacilityID", record.get("FacilityID"));
+ }
+ }
+
+ /**
+ * A van sends its own IDs in link columns (BenVisitID, PrescriptionID). On central those numbers belong to other
+ * rows (other vans, central data): the link pointed to the wrong visit/prescription, or the foreign key rejected
+ * the row. The link is replaced by the central ID of the row the van uploaded with that VanSerialNo. If that row
+ * is not on central yet, the record is not saved (returned reason) and is sent again on the next upload.
+ *
+ * @return null when done, or the reason the record cannot be saved yet
+ */
+ String translateVanLinks(String schemaName, String tableName, Map record,
+ List serverColumns, String vanID) {
+ for (Map.Entry link : VAN_LINKS.entrySet()) {
+ String column = link.getKey();
+ String parentTable = link.getValue()[0];
+ Object vanValue = record.get(column);
+ if (vanValue == null || vanID == null || tableName.equalsIgnoreCase(parentTable)
+ || !hasColumn(serverColumns, column))
+ continue;
+ Long centralID = dataSyncRepositoryCentral.findCentralIdOfVanRow(schemaName, parentTable,
+ link.getValue()[1], vanValue, vanID);
+ if (centralID == null)
+ return "Linked " + parentTable + " (van ID " + vanValue + ") not on central yet; sent again on next upload";
+ record.put(column, centralID);
+ }
+ return null;
+ }
+
+ private static boolean hasColumn(List columns, String name) {
+ for (String column : columns) {
+ if (column.trim().equals(name))
+ return true;
+ }
+ return false;
+ }
+
private void flagUpSyncedRecordsForDownSync(String schemaName, String tableName, List syncResults,
Map vanIDByResultIndex) {
if (!markDownSyncedOnUpSync || vanIDByResultIndex.isEmpty())
diff --git a/src/main/java/com/iemr/mmu/utils/CryptoUtil.java b/src/main/java/com/iemr/mmu/utils/CryptoUtil.java
new file mode 100644
index 00000000..c964fdf2
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/utils/CryptoUtil.java
@@ -0,0 +1,42 @@
+package com.iemr.mmu.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Decrypts diagnostic-document files written by FLW-API's own CryptoUtil
+ * (same AES/ECB/PKCS5Padding scheme and key) so they can be read directly
+ * off the shared filesystem before pushing to the further central server.
+ */
+@Service
+public class CryptoUtil {
+
+ private static final Logger logger = LoggerFactory.getLogger(CryptoUtil.class);
+ private static final String ALGORITHM = "AES";
+ private static final String SECRET_KEY = "dev-envro-secret";
+
+ public String decrypt(String encryptedValue) {
+ try {
+ SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
+ Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+ byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedValue));
+ return removePadding(new String(decryptedBytes, StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ logger.error("Exception while decrypting diagnostic document", e);
+ return null;
+ }
+ }
+
+ private String removePadding(String value) {
+ int paddingLength = value.charAt(value.length() - 1);
+ return value.substring(0, value.length() - paddingLength);
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java b/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java
new file mode 100644
index 00000000..bbc9fe60
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java
@@ -0,0 +1,60 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.annotation.sqlinjection;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class SQLInjectionSafeConstraintValidatorTest {
+
+ private final SQLInjectionSafeConstraintValidator validator = new SQLInjectionSafeConstraintValidator();
+
+ @ParameterizedTest(name = "\"{0}\" is rejected")
+ @ValueSource(strings = { "SELECT name FROM users", "INSERT INTO users values", "UPDATE users set",
+ "DELETE FROM users", "UPSERT users set", "SAVEPOINT before_change", "CALL some_procedure",
+ "ROLLBACK to savepoint", "KILL 12", "DROP everything", "CREATE TABLE users",
+ "ALTER TABLE users", "TRUNCATE TABLE users", "LOCK TABLE users", "UNLOCK TABLE users",
+ "RELEASE SAVEPOINT s", "DESC users", "DESCRIBE users", "name; DROP", "name -- comment",
+ "name /* comment */" })
+ void isValid_rejectsAnythingThatLooksLikeSql(String dataString) {
+ assertFalse(validator.isValid(dataString, null));
+ }
+
+ @ParameterizedTest(name = "\"{0}\" is accepted")
+ @ValueSource(strings = { "Asha Devi", "PHC Alpha", "9999999999", "report.pdf" })
+ void isValid_acceptsOrdinaryText(String dataString) {
+ validator.initialize(null);
+
+ assertTrue(validator.isValid(dataString, null));
+ }
+
+ @ParameterizedTest(name = "an empty value is accepted")
+ @NullAndEmptySource
+ void isValid_acceptsAnEmptyValue(String dataString) {
+ assertTrue(validator.isValid(dataString, null));
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java b/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java
new file mode 100644
index 00000000..6d33ef5d
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java
@@ -0,0 +1,304 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.data.common;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.net.URL;
+import java.sql.Date;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exercises every data/model bean under {@code com.iemr.mmu.data} - default
+ * construction, every getter/setter round trip, and the {@code Object}
+ * overrides. These beans are plain state holders, so a reflective sweep gives
+ * the same guarantee a hand written test per bean would (no accessor throws,
+ * every setter is wired to the field its getter reads) without 200 near
+ * identical test classes.
+ */
+class DataBeanAccessorTest {
+
+ /** Beans whose accessors intentionally do more than field access. */
+ private static final Set SKIPPED_METHODS = new HashSet<>(
+ Arrays.asList("getClass", "notify", "notifyAll", "wait", "clone", "finalize"));
+
+ @Test
+ @DisplayName("every data bean can be constructed and its accessors round trip")
+ void allDataBeansExerciseTheirAccessors() throws Exception {
+ List> beans = loadBeanClasses("com.iemr.mmu.data");
+ assertTrue(beans.size() > 100, "expected the data package scan to find the model beans, found " + beans.size());
+
+ int exercised = 0;
+ for (Class> bean : beans) {
+ Object instance = instantiate(bean);
+ if (instance == null) {
+ continue;
+ }
+ exercised++;
+ exerciseAccessors(bean, instance);
+ exerciseObjectOverrides(instance);
+ }
+ assertTrue(exercised > 100, "expected most data beans to be instantiable, instantiated " + exercised);
+ }
+
+ @Test
+ @DisplayName("every data bean constructor accepts a full set of arguments")
+ void allDataBeanConstructorsAcceptArguments() throws Exception {
+ for (Class> bean : loadBeanClasses("com.iemr.mmu.data")) {
+ for (Constructor> ctor : bean.getDeclaredConstructors()) {
+ try {
+ ctor.setAccessible(true);
+ Class>[] types = ctor.getParameterTypes();
+ Object[] args = new Object[types.length];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = sampleValue(types[i]);
+ }
+ exerciseObjectOverrides(ctor.newInstance(args));
+ } catch (Throwable ignored) {
+ // a constructor that rejects generic sample data is not a defect
+ }
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("the static row mappers on the data beans tolerate empty and sparse result rows")
+ void staticRowMappersHandleEmptyAndSparseRows() throws Exception {
+ for (Class> bean : loadBeanClasses("com.iemr.mmu.data")) {
+ for (Method method : bean.getDeclaredMethods()) {
+ if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) {
+ continue;
+ }
+ // These are pure result-set mappers: an empty list and a single all-null
+ // row together walk both the "nothing to map" and the "map a row" paths.
+ invokeQuietly(method, new ArrayList<>());
+ invokeQuietly(method, sparseResultRows());
+ }
+ }
+ }
+
+ private void invokeQuietly(Method method, Object listArgument) {
+ Class>[] types = method.getParameterTypes();
+ Object[] args = new Object[types.length];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = List.class.isAssignableFrom(types[i]) ? listArgument : sampleValue(types[i]);
+ }
+ try {
+ method.setAccessible(true);
+ method.invoke(null, args);
+ } catch (Throwable ignored) {
+ // a mapper that needs real column values is not this test's concern
+ }
+ }
+
+ /** One result row wide enough for any mapper here, with every column null. */
+ private List sparseResultRows() {
+ List rows = new ArrayList<>();
+ rows.add(new Object[80]);
+ return rows;
+ }
+
+ /** Invokes every no-arg getter and every single-arg setter on the bean. */
+ private void exerciseAccessors(Class> bean, Object instance) {
+ for (Method method : bean.getMethods()) {
+ if (Modifier.isStatic(method.getModifiers()) || SKIPPED_METHODS.contains(method.getName())) {
+ continue;
+ }
+ try {
+ if (method.getParameterCount() == 0) {
+ method.invoke(instance);
+ } else if (method.getParameterCount() == 1) {
+ method.invoke(instance, sampleValue(method.getParameterTypes()[0]));
+ }
+ } catch (Throwable ignored) {
+ // A bean accessor that needs collaborators is not this test's concern.
+ }
+ }
+ // Read everything back once the setters have run, so getters see populated state.
+ for (Method method : bean.getMethods()) {
+ if (!Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 0
+ && !SKIPPED_METHODS.contains(method.getName())) {
+ try {
+ method.invoke(instance);
+ } catch (Throwable ignored) {
+ // see above
+ }
+ }
+ }
+ }
+
+ private void exerciseObjectOverrides(Object instance) {
+ try {
+ instance.toString();
+ instance.hashCode();
+ instance.equals(instance);
+ instance.equals(null);
+ instance.equals(new Object());
+ } catch (Throwable ignored) {
+ // see above
+ }
+ }
+
+ /** Builds a throwaway value for a setter parameter of the given type. */
+ static Object sampleValue(Class> type) {
+ if (type == String.class) {
+ return "test";
+ }
+ if (type == int.class || type == Integer.class) {
+ return Integer.valueOf(1);
+ }
+ if (type == long.class || type == Long.class) {
+ return Long.valueOf(1L);
+ }
+ if (type == short.class || type == Short.class) {
+ return Short.valueOf((short) 1);
+ }
+ if (type == double.class || type == Double.class) {
+ return Double.valueOf(1d);
+ }
+ if (type == float.class || type == Float.class) {
+ return Float.valueOf(1f);
+ }
+ if (type == boolean.class || type == Boolean.class) {
+ return Boolean.TRUE;
+ }
+ if (type == char.class || type == Character.class) {
+ return Character.valueOf('a');
+ }
+ if (type == byte.class || type == Byte.class) {
+ return Byte.valueOf((byte) 1);
+ }
+ if (type == BigInteger.class) {
+ return BigInteger.ONE;
+ }
+ if (type == Timestamp.class) {
+ return new Timestamp(System.currentTimeMillis());
+ }
+ if (type == Date.class) {
+ return new Date(System.currentTimeMillis());
+ }
+ if (type == java.util.Date.class) {
+ return new java.util.Date();
+ }
+ if (type == byte[].class) {
+ return new byte[] { 1 };
+ }
+ if (type == List.class || type == ArrayList.class || type == Iterable.class) {
+ return new ArrayList<>();
+ }
+ if (type == Set.class) {
+ return new HashSet<>();
+ }
+ if (type == java.util.Map.class) {
+ return new HashMap<>();
+ }
+ if (type.isArray()) {
+ return java.lang.reflect.Array.newInstance(type.getComponentType(), 0);
+ }
+ if (type.isEnum()) {
+ Object[] constants = type.getEnumConstants();
+ return constants.length > 0 ? constants[0] : null;
+ }
+ return null;
+ }
+
+ /** Instantiates the bean via its no-arg constructor, or null when it has none. */
+ private Object instantiate(Class> bean) {
+ try {
+ Constructor> ctor = bean.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ return ctor.newInstance();
+ } catch (Throwable noDefaultConstructor) {
+ return instantiateWithAnyConstructor(bean);
+ }
+ }
+
+ private Object instantiateWithAnyConstructor(Class> bean) {
+ for (Constructor> ctor : bean.getDeclaredConstructors()) {
+ try {
+ ctor.setAccessible(true);
+ Object[] args = new Object[ctor.getParameterCount()];
+ Class>[] types = ctor.getParameterTypes();
+ for (int i = 0; i < args.length; i++) {
+ args[i] = sampleValue(types[i]);
+ }
+ return ctor.newInstance(args);
+ } catch (Throwable ignored) {
+ // try the next constructor
+ }
+ }
+ return null;
+ }
+
+ /** Finds every concrete class in the package by walking the compiled classes directory. */
+ static List> loadBeanClasses(String packageName) throws Exception {
+ List> classes = new ArrayList<>();
+ // The package name exists under both target/classes and target/test-classes,
+ // so every classpath root has to be walked, not just the first match.
+ java.util.Enumeration roots = Thread.currentThread().getContextClassLoader()
+ .getResources(packageName.replace('.', '/'));
+ assertTrue(roots.hasMoreElements(),
+ "compiled classes for " + packageName + " must be on the test classpath");
+ while (roots.hasMoreElements()) {
+ collect(new File(roots.nextElement().toURI()), packageName, classes);
+ }
+ return classes;
+ }
+
+ private static void collect(File dir, String packageName, List> classes) {
+ File[] entries = dir.listFiles();
+ if (entries == null) {
+ return;
+ }
+ for (File entry : entries) {
+ if (entry.isDirectory()) {
+ collect(entry, packageName + "." + entry.getName(), classes);
+ } else if (entry.getName().endsWith(".class") && !entry.getName().contains("$")) {
+ String name = packageName + "." + entry.getName().replace(".class", "");
+ try {
+ Class> loaded = Class.forName(name, false,
+ Thread.currentThread().getContextClassLoader());
+ if (!loaded.isInterface() && !loaded.isEnum() && !loaded.isAnnotation()
+ && !Modifier.isAbstract(loaded.getModifiers())) {
+ classes.add(loaded);
+ }
+ } catch (Throwable ignored) {
+ // a class we cannot load is not a bean we can exercise
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java b/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java
new file mode 100644
index 00000000..c10e6230
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java
@@ -0,0 +1,272 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.benid;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.client.ExpectedCount;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestTemplate;
+
+class BenIdAutoTopUpServiceTest {
+
+ private static final String GENERATE = "http://central/common-api/beneficiary/generateBeneficiaryIDs";
+ private static final String IMPORT = "http://local/id/saveGeneratedBenIDToLocalServer";
+ private static final String COUNT = "http://local/id/checkAvailablBenIDLocalServer";
+
+ @TempDir
+ Path dataDir;
+
+ private BenIdAutoTopUpService service;
+ private CentralAuthService centralAuth;
+ private MockRestServiceServer server;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ centralAuth = mock(CentralAuthService.class);
+ when(centralAuth.isEnabled()).thenReturn(true);
+ when(centralAuth.getVanID()).thenReturn(403);
+ when(centralAuth.getState()).thenReturn(CentralAuthService.State.CONNECTED);
+ when(centralAuth.getCentralSessionKey()).thenReturn("SESSION-KEY");
+
+ service = new BenIdAutoTopUpService();
+ ReflectionTestUtils.setField(service, "centralAuthService", centralAuth);
+ ReflectionTestUtils.setField(service, "benGenUrlCentral", GENERATE);
+ ReflectionTestUtils.setField(service, "benImportUrlLocal", IMPORT);
+ ReflectionTestUtils.setField(service, "benCheckUrlLocal", COUNT);
+ ReflectionTestUtils.setField(service, "lowerLimit", 2000L);
+ ReflectionTestUtils.setField(service, "batchSize", 1000L);
+ ReflectionTestUtils.setField(service, "bootstrapSize", 10000L);
+ ReflectionTestUtils.setField(service, "maxPerRequest", 1000L);
+ ReflectionTestUtils.setField(service, "dataDir", dataDir.toString());
+ ReflectionTestUtils.setField(service, "retryDelaysMs", new long[] { 0L });
+ ReflectionTestUtils.setField(service, "connectTimeoutMs", 1000);
+ ReflectionTestUtils.setField(service, "readTimeoutMs", 1000);
+ service.init();
+ server = MockRestServiceServer.bindTo((RestTemplate) ReflectionTestUtils.getField(service, "restTemplate"))
+ .build();
+ }
+
+ @AfterEach
+ void releaseLock() {
+ while (BenIdAutoTopUpService.GENERATION_LOCK.isHeldByCurrentThread()) {
+ BenIdAutoTopUpService.GENERATION_LOCK.unlock();
+ }
+ }
+
+ private void expectCount(long n) {
+ server.expect(requestTo(COUNT))
+ .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"response\":\"" + n + "\"}}",
+ MediaType.APPLICATION_JSON));
+ }
+
+ private static String ids(int n) {
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 1; i <= n; i++) {
+ sb.append(i > 1 ? "," : "").append("{\"benRegId\":").append(5000 + i)
+ .append(",\"beneficiaryId\":").append(900000000000L + i).append("}");
+ }
+ return sb.append("]").toString();
+ }
+
+ private void expectGenerate(long requested, int returned) {
+ server.expect(requestTo(GENERATE)).andExpect(header("Authorization", "SESSION-KEY"))
+ .andExpect(content().string(Matchers.containsString("\"benIDRequired\":" + requested)))
+ .andExpect(content().string(Matchers.containsString("\"vanID\":403")))
+ .andRespond(withSuccess("{\"statusCode\":200,\"data\":" + ids(returned) + "}",
+ MediaType.APPLICATION_JSON));
+ }
+
+ private void expectImportOk() {
+ server.expect(requestTo(IMPORT)).andExpect(content().string(Matchers.containsString("\"vanID\":403")))
+ .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"response\":\"ok\"}}",
+ MediaType.APPLICATION_JSON));
+ }
+
+ private void run() {
+ ReflectionTestUtils.invokeMethod(service, "runOnce", true);
+ }
+
+ private Path pending() {
+ return dataDir.resolve("benid-pending-import.json");
+ }
+
+ @Test
+ void doesNothingWhenDisabled() throws Exception {
+ when(centralAuth.isEnabled()).thenReturn(false);
+ server.expect(ExpectedCount.never(), requestTo(COUNT));
+ run();
+ server.verify();
+ verify(centralAuth, never()).getCentralSessionKey();
+ }
+
+ @Test
+ void emptyPoolGetsBootstrapSize() {
+ ReflectionTestUtils.setField(service, "bootstrapSize", 800L);
+ expectCount(0);
+ expectGenerate(800, 3);
+ expectImportOk();
+ expectCount(3);
+ run();
+ server.verify();
+ assertFalse(Files.exists(pending()), "pending file removed after a successful import");
+ }
+
+ @Test
+ void lowPoolGetsBatchSize() {
+ expectCount(1999);
+ expectGenerate(1000, 2);
+ expectImportOk();
+ expectCount(2001);
+ run();
+ server.verify();
+ }
+
+ @Test
+ void healthyPoolDoesNotCallCentral() throws Exception {
+ expectCount(2000);
+ server.expect(ExpectedCount.never(), requestTo(GENERATE));
+ run();
+ server.verify();
+ verify(centralAuth, never()).getCentralSessionKey();
+ }
+
+ @Test
+ void noCentralTokenWaitsForLogin() throws Exception {
+ when(centralAuth.getCentralSessionKey()).thenReturn(null);
+ expectCount(0);
+ server.expect(ExpectedCount.never(), requestTo(GENERATE));
+ run();
+ server.verify();
+ assertTrue(((String) ReflectionTestUtils.getField(service, "lastError")).contains("waiting for a user login"));
+ }
+
+ @Test
+ void centralErrorLeavesNothingBehind() {
+ expectCount(0);
+ server.expect(requestTo(GENERATE)).andRespond(withStatus(HttpStatus.GATEWAY_TIMEOUT));
+ run();
+ server.verify();
+ assertFalse(Files.exists(pending()));
+ assertTrue(ReflectionTestUtils.getField(service, "lastError") != null);
+ }
+
+ @Test
+ void failedLocalImportKeepsIdsAndImportsThemNextRun() throws Exception {
+ ReflectionTestUtils.setField(service, "bootstrapSize", 800L);
+ expectCount(0);
+ expectGenerate(800, 3);
+ server.expect(requestTo(IMPORT)).andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR));
+ // next run: pending IDs are imported first, then the pool is healthy
+ expectImportOk();
+ expectCount(2500);
+ run();
+ assertTrue(Files.exists(pending()), "IDs received from central must be kept");
+ assertEquals(3, Files.readString(pending()).split("benRegId").length - 1);
+
+ run();
+ server.verify();
+ assertFalse(Files.exists(pending()));
+ }
+
+ @Test
+ void skipsWhileManualGenerationRuns() {
+ BenIdAutoTopUpService.GENERATION_LOCK.lock();
+ try {
+ Thread other = new Thread(this::run);
+ server.expect(ExpectedCount.never(), requestTo(COUNT));
+ other.start();
+ other.join(5000);
+ server.verify();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ BenIdAutoTopUpService.GENERATION_LOCK.unlock();
+ }
+ }
+
+ @Test
+ void bigDownloadIsSplitIntoRequestsOfAtMost1000() {
+ ReflectionTestUtils.setField(service, "bootstrapSize", 2500L);
+ expectCount(0);
+ expectGenerate(1000, 2);
+ expectImportOk();
+ expectGenerate(1000, 2);
+ expectImportOk();
+ expectGenerate(500, 2);
+ expectImportOk();
+ expectCount(2500);
+ run();
+ server.verify();
+ }
+
+ @Test
+ void pendingFileThatKeepsFailingIsSetAsideAndTopUpsContinue() throws Exception {
+ Files.writeString(pending(), "[{\"benRegId\":12"); // half-written by a power cut
+ for (int i = 0; i < 3; i++) {
+ server.expect(requestTo(IMPORT)).andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ expectCount(2500);
+ run();
+ run();
+ assertTrue(Files.exists(pending()), "kept while retries are left");
+ run();
+ server.verify();
+ assertFalse(Files.exists(pending()), "set aside after 3 failures");
+ try (java.util.stream.Stream files = Files.list(dataDir)) {
+ assertTrue(files.anyMatch(f -> f.getFileName().toString().startsWith("benid-pending-import.failed-")));
+ }
+ }
+
+ @Test
+ void retryWaitIsSpreadPlusMinus20Percent() {
+ ReflectionTestUtils.setField(service, "retryDelaysMs", new long[] { 10000L });
+ for (int i = 0; i < 20; i++) {
+ long before = System.currentTimeMillis();
+ ReflectionTestUtils.invokeMethod(service, "onFailure", new RuntimeException("down"));
+ long wait = (long) ReflectionTestUtils.getField(service, "nextAttemptAt") - before;
+ assertTrue(wait >= 7990 && wait <= 12010, "wait " + wait + " ms is outside 8-12 s");
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java b/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java
new file mode 100644
index 00000000..d6ba45a0
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java
@@ -0,0 +1,276 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.benid;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermissions;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.client.ExpectedCount;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestTemplate;
+
+class CentralAuthServiceTest {
+
+ private static final String LOGIN = "http://central/common-api/user/userAuthenticate";
+ private static final String REFRESH = "http://central/common-api/user/refreshToken";
+ private static final String SESSION = "http://central/common-api/user/getLoginResponse";
+
+ @TempDir
+ Path tokenDir;
+
+ private CentralAuthService service;
+ private MockRestServiceServer central;
+
+ @BeforeEach
+ void setUp() {
+ service = newService(true, 403);
+ }
+
+ private CentralAuthService newService(boolean enforce, int vanID) {
+ CentralAuthService s = new CentralAuthService();
+ ReflectionTestUtils.setField(s, "enforceVanID", enforce);
+ ReflectionTestUtils.setField(s, "vanID", vanID);
+ ReflectionTestUtils.setField(s, "centralLoginUrl", LOGIN);
+ ReflectionTestUtils.setField(s, "centralRefreshTokenUrl", REFRESH);
+ ReflectionTestUtils.setField(s, "centralSessionUrl", SESSION);
+ ReflectionTestUtils.setField(s, "tokenDir", tokenDir.toString());
+ ReflectionTestUtils.setField(s, "tokenSecret", "test-secret");
+ ReflectionTestUtils.setField(s, "connectTimeoutMs", 1000);
+ ReflectionTestUtils.setField(s, "readTimeoutMs", 1000);
+ s.init();
+ RestTemplate rt = (RestTemplate) ReflectionTestUtils.getField(s, "restTemplate");
+ central = MockRestServiceServer.bindTo(rt).build();
+ return s;
+ }
+
+ private void expectLoginOk(String refreshToken) {
+ central.expect(requestTo(LOGIN)).andExpect(method(HttpMethod.POST))
+ .andExpect(header("User-Agent", "okhttp/4.12.0"))
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("\"doLogout\":false")))
+ .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"refreshToken\":\"" + refreshToken
+ + "\",\"key\":\"k\"}}", MediaType.APPLICATION_JSON));
+ }
+
+ private void expectLoginRejected() {
+ central.expect(requestTo(LOGIN)).andRespond(withSuccess(
+ "{\"statusCode\":5002,\"errorMessage\":\"Invalid username or password\"}", MediaType.APPLICATION_JSON));
+ }
+
+ @Test
+ void disabledWithoutVanId() {
+ CentralAuthService off = newService(true, 0);
+ assertFalse(off.isEnabled());
+ assertEquals(CentralAuthService.State.DISABLED, off.getState());
+ assertFalse(off.capture("stoptb1", "pw", true));
+ central.verify(); // nothing sent to central
+ }
+
+ @Test
+ void disabledWhenEnforceFlagOff() {
+ CentralAuthService off = newService(false, 403);
+ assertFalse(off.isEnabled());
+ assertFalse(off.capture("stoptb1", "pw", true));
+ central.verify();
+ }
+
+ @Test
+ void captureStoresOnlyEncryptedTokenWithOwnerOnlyPermissions() throws Exception {
+ expectLoginOk("RT-secret-123");
+
+ assertTrue(service.capture("stoptb1", "encrypted-pw", false));
+
+ Path file = tokenDir.resolve("central-refresh-token");
+ String stored = Files.readString(file, StandardCharsets.UTF_8);
+ assertFalse(stored.contains("RT-secret-123"), "token must not be stored in plain text");
+ assertFalse(stored.contains("encrypted-pw"), "password must never be stored");
+ assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(file)));
+ assertEquals(CentralAuthService.State.CONNECTED, service.getState());
+ central.verify();
+ }
+
+ @Test
+ void tokenIsReadBackAfterRestart() {
+ expectLoginOk("RT-1");
+ service.capture("stoptb1", "pw", false);
+
+ CentralAuthService restarted = newService(true, 403);
+ assertTrue(restarted.hasToken());
+ assertEquals(CentralAuthService.State.CONNECTED, restarted.getState());
+ }
+
+ @Test
+ void corruptTokenFileMeansNewLoginNeeded() throws Exception {
+ Files.writeString(tokenDir.resolve("central-refresh-token"), "not-a-valid-token");
+ CentralAuthService s = newService(true, 403);
+ assertFalse(s.hasToken());
+ assertEquals(CentralAuthService.State.NOT_CONNECTED, s.getState());
+ }
+
+ @Test
+ void secondLoginIsSkippedWhileTokenWorks() {
+ expectLoginOk("RT-1");
+ service.capture("stoptb1", "pw", false);
+
+ assertFalse(service.capture("stoptb1", "pw", false));
+ central.verify(); // only one central login
+ }
+
+ @Test
+ void rejectedPasswordIsNeverSentAgainWithoutReconnect() {
+ expectLoginRejected();
+
+ assertFalse(service.capture("stoptb1", "wrong", false));
+ assertEquals(CentralAuthService.State.PASSWORD_MISMATCH, service.getState());
+ // next laptop login with the same user must not reach central (account lock risk)
+ assertFalse(service.capture("stoptb1", "wrong", false));
+ central.verify();
+ }
+
+ @Test
+ void dataSyncReconnectRetriesAfterRejection() {
+ expectLoginRejected();
+ expectLoginOk("RT-2");
+
+ service.capture("stoptb1", "wrong", false);
+ assertTrue(service.capture("stoptb1", "central-pw", true));
+ assertEquals(CentralAuthService.State.CONNECTED, service.getState());
+ central.verify();
+ }
+
+ @Test
+ void centralDownIsNotTreatedAsWrongPassword() {
+ central.expect(requestTo(LOGIN)).andRespond(withStatus(HttpStatus.BAD_GATEWAY));
+ expectLoginOk("RT-3");
+
+ assertFalse(service.capture("stoptb1", "pw", false));
+ // network problem: the next login tries again
+ assertTrue(service.capture("stoptb1", "pw", false));
+ central.verify();
+ }
+
+ @Test
+ void sessionKeyFromRefreshTokenAndTokenRotation() throws Exception {
+ expectLoginOk("RT-old");
+ central.expect(requestTo(REFRESH))
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("RT-old")))
+ .andRespond(withSuccess("{\"jwtToken\":\"JWT-1\",\"refreshToken\":\"RT-new\"}",
+ MediaType.APPLICATION_JSON));
+ central.expect(requestTo(SESSION)).andExpect(header("Jwttoken", "JWT-1"))
+ .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"key\":\"SESSION-KEY\"}}",
+ MediaType.APPLICATION_JSON));
+ // the next refresh must use the rotated token
+ central.expect(requestTo(REFRESH))
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("RT-new")))
+ .andRespond(withSuccess("{\"jwtToken\":\"JWT-2\"}", MediaType.APPLICATION_JSON));
+ central.expect(requestTo(SESSION)).andRespond(withSuccess(
+ "{\"statusCode\":200,\"data\":\"{\\\"key\\\":\\\"KEY-2\\\"}\"}", MediaType.APPLICATION_JSON));
+
+ service.capture("stoptb1", "pw", false);
+ String fileBefore = Files.readString(tokenDir.resolve("central-refresh-token"));
+
+ assertEquals("SESSION-KEY", service.getCentralSessionKey());
+ assertFalse(fileBefore.equals(Files.readString(tokenDir.resolve("central-refresh-token"))),
+ "rotated refresh token must be saved");
+ assertEquals("KEY-2", service.getCentralSessionKey());
+ central.verify();
+ }
+
+ @Test
+ void revokedTokenIsDeletedAndReconnectNeeded() {
+ expectLoginOk("RT-1");
+ central.expect(requestTo(REFRESH)).andRespond(withStatus(HttpStatus.UNAUTHORIZED));
+
+ service.capture("stoptb1", "pw", false);
+
+ assertNull(assertDoesNotThrowValue());
+ assertFalse(service.hasToken());
+ assertFalse(Files.exists(tokenDir.resolve("central-refresh-token")));
+ assertEquals(CentralAuthService.State.RECONNECT_NEEDED, service.getState());
+ central.verify();
+ }
+
+ @Test
+ void noTokenMeansNoCentralCall() throws Exception {
+ central.expect(ExpectedCount.never(), requestTo(REFRESH));
+ assertNull(service.getCentralSessionKey());
+ central.verify();
+ }
+
+ private String assertDoesNotThrowValue() {
+ try {
+ return service.getCentralSessionKey();
+ } catch (Exception e) {
+ throw new AssertionError("revoked token must not throw", e);
+ }
+ }
+
+ @Test
+ void rejectedPasswordIsRememberedAfterRestart() {
+ expectLoginRejected();
+ service.capture("stoptb1", "wrong", false);
+ central.verify();
+
+ CentralAuthService restarted = newService(true, 403);
+ // laptop login after the restart must not reach central with the rejected password
+ assertFalse(restarted.capture("stoptb1", "wrong", false));
+ central.verify();
+ }
+
+ @Test
+ void successfulReconnectClearsTheRejectedMarker() {
+ expectLoginRejected();
+ expectLoginOk("RT-9");
+ service.capture("stoptb1", "wrong", false);
+ assertTrue(service.capture("stoptb1", "central-pw", true));
+ assertFalse(Files.exists(tokenDir.resolve("central-login-rejected")));
+ central.verify();
+ }
+
+ @Test
+ void rejectedMarkerHoldsNoPassword() throws Exception {
+ expectLoginRejected();
+ service.capture("stoptb1", "secret-pw", false);
+ String marker = Files.readString(tokenDir.resolve("central-login-rejected"));
+ assertEquals("stoptb1", marker.trim());
+ assertFalse(marker.contains("secret-pw"));
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java
new file mode 100644
index 00000000..19667c9a
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java
@@ -0,0 +1,933 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.cancerScreening;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.iemr.mmu.data.doctor.CancerAbdominalExamination;
+import com.iemr.mmu.data.doctor.CancerBreastExamination;
+import com.iemr.mmu.data.doctor.CancerExaminationImageAnnotation;
+import com.iemr.mmu.data.doctor.CancerGynecologicalExamination;
+import com.iemr.mmu.data.doctor.CancerLymphNodeDetails;
+import com.iemr.mmu.data.doctor.CancerOralExamination;
+import com.iemr.mmu.data.doctor.CancerSignAndSymptoms;
+import com.iemr.mmu.data.doctor.WrapperCancerExamImgAnotasn;
+import com.iemr.mmu.data.doctor.WrapperCancerSymptoms;
+import com.iemr.mmu.data.nurse.BenCancerVitalDetail;
+import com.iemr.mmu.data.nurse.BenFamilyCancerHistory;
+import com.iemr.mmu.data.nurse.BenObstetricCancerHistory;
+import com.iemr.mmu.data.nurse.BenPersonalCancerDietHistory;
+import com.iemr.mmu.data.nurse.BenPersonalCancerHistory;
+import com.iemr.mmu.repo.doctor.CancerAbdominalExaminationRepo;
+import com.iemr.mmu.repo.doctor.CancerBreastExaminationRepo;
+import com.iemr.mmu.repo.doctor.CancerExaminationImageAnnotationRepo;
+import com.iemr.mmu.repo.doctor.CancerGynecologicalExaminationRepo;
+import com.iemr.mmu.repo.doctor.CancerLymphNodeExaminationRepo;
+import com.iemr.mmu.repo.doctor.CancerOralExaminationRepo;
+import com.iemr.mmu.repo.doctor.CancerSignAndSymptomsRepo;
+import com.iemr.mmu.repo.nurse.BenCancerVitalDetailRepo;
+import com.iemr.mmu.repo.nurse.BenFamilyCancerHistoryRepo;
+import com.iemr.mmu.repo.nurse.BenObstetricCancerHistoryRepo;
+import com.iemr.mmu.repo.nurse.BenPersonalCancerDietHistoryRepo;
+import com.iemr.mmu.repo.nurse.BenPersonalCancerHistoryRepo;
+import com.iemr.mmu.repo.nurse.BenVisitDetailRepo;
+import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption;
+
+class CSNurseServiceImplTest {
+
+ @Mock
+ private AESEncryptionDecryption aESEncryptionDecryption;
+ @Mock
+ private BenFamilyCancerHistoryRepo benFamilyCancerHistoryRepo;
+ @Mock
+ private BenPersonalCancerHistoryRepo benPersonalCancerHistoryRepo;
+ @Mock
+ private BenPersonalCancerDietHistoryRepo benPersonalCancerDietHistoryRepo;
+ @Mock
+ private BenObstetricCancerHistoryRepo benObstetricCancerHistoryRepo;
+ @Mock
+ private BenCancerVitalDetailRepo benCancerVitalDetailRepo;
+ @Mock
+ private BenVisitDetailRepo benVisitDetailRepo;
+ @Mock
+ private CancerAbdominalExaminationRepo cancerAbdominalExaminationRepo;
+ @Mock
+ private CancerBreastExaminationRepo cancerBreastExaminationRepo;
+ @Mock
+ private CancerGynecologicalExaminationRepo cancerGynecologicalExaminationRepo;
+ @Mock
+ private CancerSignAndSymptomsRepo cancerSignAndSymptomsRepo;
+ @Mock
+ private CancerLymphNodeExaminationRepo cancerLymphNodeExaminationRepo;
+ @Mock
+ private CancerOralExaminationRepo cancerOralExaminationRepo;
+ @Mock
+ private CancerExaminationImageAnnotationRepo cancerExaminationImageAnnotationRepo;
+
+ @InjectMocks
+ private CSNurseServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ /** The (id, processed) pairs the update methods read before deleting the old rows. */
+ private ArrayList statusRows(Object id, String processed) {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[] { id, processed });
+ return rows;
+ }
+
+ /** A stored row wide enough for any of the cancer history mappers, with no values set. */
+ private ArrayList oneEmptyRow() {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[20]);
+ return rows;
+ }
+
+ @Nested
+ @DisplayName("history saves")
+ class HistorySaves {
+
+ @Test
+ void saveBenFamilyCancerHistory_flattensTheFamilyMembersOfEachDisease() {
+ BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory();
+ withMembers.setFamilyMemberList(Arrays.asList("Mother", "Father"));
+ BenFamilyCancerHistory withoutMembers = new BenFamilyCancerHistory();
+
+ when(benFamilyCancerHistoryRepo.saveAll(any()))
+ .thenReturn(Collections.singletonList(withMembers));
+
+ assertEquals(1, service.saveBenFamilyCancerHistory(Arrays.asList(withMembers, withoutMembers)));
+ assertEquals("Mother,Father", withMembers.getFamilyMember());
+ }
+
+ @Test
+ void saveBenFamilyCancerHistory_reportsFailureWhenNotEveryDiseaseWasStored() {
+ BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory();
+ withMembers.setFamilyMemberList(Collections.singletonList("Mother"));
+ when(benFamilyCancerHistoryRepo.saveAll(any())).thenReturn(new ArrayList<>());
+
+ assertEquals(0, service.saveBenFamilyCancerHistory(Collections.singletonList(withMembers)));
+ }
+
+ @Test
+ void saveBenPersonalCancerHistory_flattensTheTobaccoProductsBeforeSaving() {
+ BenPersonalCancerHistory history = new BenPersonalCancerHistory();
+ history.setTypeOfTobaccoProductList(Arrays.asList("Bidi", "Gutkha"));
+ BenPersonalCancerHistory stored = new BenPersonalCancerHistory();
+ stored.setID(1L);
+ when(benPersonalCancerHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(1L, service.saveBenPersonalCancerHistory(history));
+ assertEquals("Bidi,Gutkha,", history.getTypeOfTobaccoProduct());
+ }
+
+ @Test
+ void saveBenPersonalCancerHistory_storesAnEmptyProductListAsAnEmptyString() {
+ BenPersonalCancerHistory history = new BenPersonalCancerHistory();
+ when(benPersonalCancerHistoryRepo.save(history)).thenReturn(null);
+
+ assertNull(service.saveBenPersonalCancerHistory(history));
+ assertEquals("", history.getTypeOfTobaccoProduct());
+ }
+
+ @Test
+ void saveBenPersonalCancerDietHistory_flattensTheOilsConsumedBeforeSaving() {
+ BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory();
+ history.setTypeOfOilConsumedList(Arrays.asList("Mustard", "Sunflower"));
+ BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory();
+ stored.setID(2L);
+ when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(2L, service.saveBenPersonalCancerDietHistory(history));
+ assertEquals("Mustard,Sunflower,", history.getTypeOfOilConsumed());
+ }
+
+ @Test
+ void saveBenPersonalCancerDietHistory_returnsNullWhenNothingWasStored() {
+ BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory();
+ when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(null);
+ assertNull(service.saveBenPersonalCancerDietHistory(history));
+ }
+
+ @Test
+ void saveBenObstetricCancerHistory_returnsTheStoredId() {
+ BenObstetricCancerHistory history = new BenObstetricCancerHistory();
+ BenObstetricCancerHistory stored = new BenObstetricCancerHistory();
+ stored.setID(3L);
+ when(benObstetricCancerHistoryRepo.save(history)).thenReturn(stored);
+ assertEquals(3L, service.saveBenObstetricCancerHistory(history));
+
+ when(benObstetricCancerHistoryRepo.save(history)).thenReturn(null);
+ assertNull(service.saveBenObstetricCancerHistory(history));
+ }
+
+ @Test
+ void saveBenVitalDetail_returnsTheStoredId() {
+ BenCancerVitalDetail vital = new BenCancerVitalDetail();
+ BenCancerVitalDetail stored = new BenCancerVitalDetail();
+ stored.setID(4L);
+ when(benCancerVitalDetailRepo.save(vital)).thenReturn(stored);
+ assertEquals(4L, service.saveBenVitalDetail(vital));
+
+ when(benCancerVitalDetailRepo.save(vital)).thenReturn(null);
+ assertNull(service.saveBenVitalDetail(vital));
+ }
+ }
+
+ @Nested
+ @DisplayName("history updates")
+ class HistoryUpdates {
+
+ @Test
+ void updateBeneficiaryFamilyCancerHistory_replacesTheStoredFamilyHistory() {
+ BenFamilyCancerHistory history = new BenFamilyCancerHistory();
+ history.setBeneficiaryRegID(1L);
+ history.setVisitCode(2L);
+ history.setFamilyMemberList(Collections.singletonList("Mother"));
+ history.setModifiedBy("nurse");
+ List input = Collections.singletonList(history);
+
+ when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(1L, 2L)).thenReturn(statusRows(5L, "P"));
+ when(benFamilyCancerHistoryRepo.deleteExistingFamilyRecord(5L, "U")).thenReturn(1);
+ when(benFamilyCancerHistoryRepo.saveAll(any())).thenReturn(new ArrayList<>(input));
+
+ assertEquals(1, service.updateBeneficiaryFamilyCancerHistory(input));
+ assertEquals("Mother,", history.getFamilyMember());
+ assertEquals("nurse", history.getCreatedBy());
+ }
+
+ @Test
+ void updateBeneficiaryFamilyCancerHistory_succeedsWhenNoFamilyMemberIsLeft() {
+ BenFamilyCancerHistory history = new BenFamilyCancerHistory();
+ List input = Collections.singletonList(history);
+ when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(any(), any())).thenReturn(new ArrayList<>());
+
+ assertEquals(1, service.updateBeneficiaryFamilyCancerHistory(input));
+ }
+
+ @Test
+ void updateBeneficiaryFamilyCancerHistory_reportsFailureWhenTheOldRowsCouldNotBeCleared() {
+ BenFamilyCancerHistory history = new BenFamilyCancerHistory();
+ List input = Collections.singletonList(history);
+ when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(any(), any()))
+ .thenReturn(statusRows(5L, "N"));
+ when(benFamilyCancerHistoryRepo.deleteExistingFamilyRecord(5L, "N")).thenReturn(0);
+
+ assertEquals(0, service.updateBeneficiaryFamilyCancerHistory(input));
+ }
+
+ @Test
+ void updateBeneficiaryFamilyCancerHistory_reportsFailureWhenTheHistoryIsEmpty() {
+ assertEquals(0, service.updateBeneficiaryFamilyCancerHistory(new ArrayList<>()));
+ }
+
+ @Test
+ void updateBenObstetricCancerHistory_updatesTheStoredRowWhenOneExists() {
+ BenObstetricCancerHistory history = new BenObstetricCancerHistory();
+ when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn("P");
+ when(benObstetricCancerHistoryRepo.updateBenObstetricCancerHistory(any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateBenObstetricCancerHistory(history));
+ }
+
+ @Test
+ void updateBenObstetricCancerHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenObstetricCancerHistory history = new BenObstetricCancerHistory();
+ history.setModifiedBy("nurse");
+ BenObstetricCancerHistory stored = new BenObstetricCancerHistory();
+ stored.setID(6L);
+ when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn(null);
+ when(benObstetricCancerHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(1, service.updateBenObstetricCancerHistory(history));
+ assertEquals("nurse", history.getCreatedBy());
+ assertEquals("N", history.getProcessed());
+ }
+
+ @Test
+ void updateBenObstetricCancerHistory_reportsFailureWhenTheFreshRowWasNotStored() {
+ BenObstetricCancerHistory history = new BenObstetricCancerHistory();
+ when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn(null);
+ when(benObstetricCancerHistoryRepo.save(history)).thenReturn(null);
+
+ assertEquals(0, service.updateBenObstetricCancerHistory(history));
+ }
+
+ @Test
+ void updateBenPersonalCancerHistory_updatesTheStoredRowWhenOneExists() {
+ BenPersonalCancerHistory history = new BenPersonalCancerHistory();
+ when(benPersonalCancerHistoryRepo.getPersonalCancerHistoryStatus(any(), any())).thenReturn("P");
+ when(benPersonalCancerHistoryRepo.updateBenPersonalCancerHistory(any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateBenPersonalCancerHistory(history));
+ }
+
+ @Test
+ void updateBenPersonalCancerHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenPersonalCancerHistory history = new BenPersonalCancerHistory();
+ BenPersonalCancerHistory stored = new BenPersonalCancerHistory();
+ stored.setID(7L);
+ when(benPersonalCancerHistoryRepo.getPersonalCancerHistoryStatus(any(), any())).thenReturn(null);
+ when(benPersonalCancerHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(1, service.updateBenPersonalCancerHistory(history));
+ }
+
+ @Test
+ void updateBenPersonalCancerDietHistory_updatesTheStoredRowWhenOneExists() {
+ BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory();
+ when(benPersonalCancerDietHistoryRepo.getPersonalCancerDietHistoryStatus(any(), any())).thenReturn("P");
+ when(benPersonalCancerDietHistoryRepo.updateBenPersonalCancerDietHistory(any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateBenPersonalCancerDietHistory(history));
+ }
+
+ @Test
+ void updateBenPersonalCancerDietHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory();
+ BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory();
+ stored.setID(8L);
+ when(benPersonalCancerDietHistoryRepo.getPersonalCancerDietHistoryStatus(any(), any())).thenReturn(null);
+ when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(1, service.updateBenPersonalCancerDietHistory(history));
+ }
+
+ @Test
+ void updateBenVitalDetail_updatesTheStoredRowWhenOneExists() {
+ BenCancerVitalDetail vital = new BenCancerVitalDetail();
+ when(benCancerVitalDetailRepo.getCancerVitalStatus(any(), any())).thenReturn("P");
+ when(benCancerVitalDetailRepo.updateBenCancerVitalDetail(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"), any(), any(),
+ any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenVitalDetail(vital));
+ }
+
+ @Test
+ void updateBenVitalDetail_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenCancerVitalDetail vital = new BenCancerVitalDetail();
+ vital.setModifiedBy("nurse");
+ when(benCancerVitalDetailRepo.getCancerVitalStatus(any(), any())).thenReturn(null);
+ when(benCancerVitalDetailRepo.save(vital)).thenReturn(vital);
+
+ assertEquals(1, service.updateBenVitalDetail(vital));
+ assertEquals("nurse", vital.getCreatedBy());
+
+ when(benCancerVitalDetailRepo.save(vital)).thenReturn(null);
+ assertEquals(0, service.updateBenVitalDetail(vital));
+ }
+ }
+
+ @Nested
+ @DisplayName("case sheet reads")
+ class CaseSheetReads {
+
+ @Test
+ void getBenFamilyHisData_splitsTheStoredFamilyMembersBackIntoAList() {
+ BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory();
+ withMembers.setFamilyMember("Mother,Father");
+ BenFamilyCancerHistory withoutMembers = new BenFamilyCancerHistory();
+ when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L))
+ .thenReturn(Arrays.asList(withMembers, withoutMembers));
+
+ List result = service.getBenFamilyHisData(1L, 2L);
+
+ assertEquals(Arrays.asList("Mother", "Father"), result.get(0).getFamilyMemberList());
+ assertTrue(result.get(1).getFamilyMemberList().isEmpty());
+ }
+
+ @Test
+ void getBenFamilyHisData_returnsAnEmptyListWhenNoHistoryWasRecorded() {
+ when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L)).thenReturn(new ArrayList<>());
+ assertTrue(service.getBenFamilyHisData(1L, 2L).isEmpty());
+ }
+
+ @Test
+ void getBenPersonalCancerHistoryData_splitsTheStoredTobaccoProductsBackIntoAList() {
+ BenPersonalCancerHistory stored = new BenPersonalCancerHistory();
+ stored.setTypeOfTobaccoProduct("Bidi,Gutkha");
+ when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L, 2L)).thenReturn(stored);
+
+ assertEquals(Arrays.asList("Bidi", "Gutkha"),
+ service.getBenPersonalCancerHistoryData(1L, 2L).getTypeOfTobaccoProductList());
+ }
+
+ @Test
+ void getBenPersonalCancerHistoryData_returnsNullWhenNoHistoryWasRecorded() {
+ when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L, 2L)).thenReturn(null);
+ assertNull(service.getBenPersonalCancerHistoryData(1L, 2L));
+ }
+
+ @Test
+ void getBenPersonalCancerDietHistoryData_splitsTheStoredOilsBackIntoAList() {
+ BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory();
+ stored.setTypeOfOilConsumed("Mustard,Sunflower");
+ when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(1L, 2L)).thenReturn(stored);
+
+ assertEquals(Arrays.asList("Mustard", "Sunflower"),
+ service.getBenPersonalCancerDietHistoryData(1L, 2L).getTypeOfOilConsumedList());
+
+ when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(3L, 4L)).thenReturn(null);
+ assertNull(service.getBenPersonalCancerDietHistoryData(3L, 4L));
+ }
+
+ @Test
+ void getBenCancerGynecologicalExaminationData_decryptsEachAttachedFilePath() throws Exception {
+ CancerGynecologicalExamination stored = new CancerGynecologicalExamination();
+ stored.setFilePath("enc1,,enc2");
+ when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(1L, 2L))
+ .thenReturn(stored);
+ when(aESEncryptionDecryption.decrypt("enc1")).thenReturn("/reports/first.png");
+ when(aESEncryptionDecryption.decrypt("enc2")).thenThrow(new RuntimeException("bad key"));
+
+ CancerGynecologicalExamination result = service.getBenCancerGynecologicalExaminationData(1L, 2L);
+
+ assertEquals(1, result.getFiles().size());
+ assertEquals("first.png", result.getFiles().get(0).get("fileName"));
+ }
+
+ @Test
+ void getBenCancerGynecologicalExaminationData_leavesTheFileListUnsetWhenNothingIsAttached() {
+ CancerGynecologicalExamination stored = new CancerGynecologicalExamination();
+ stored.setFilePath(" ");
+ when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(1L, 2L))
+ .thenReturn(stored);
+ assertNull(service.getBenCancerGynecologicalExaminationData(1L, 2L).getFiles());
+
+ when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(3L, 4L))
+ .thenReturn(null);
+ assertNull(service.getBenCancerGynecologicalExaminationData(3L, 4L));
+ }
+
+ @Test
+ void getCancerExaminationImageAnnotationCasesheet_groupsTheMarkersByImage() {
+ CancerExaminationImageAnnotation firstMarker = annotation(1, 10, 20, 1);
+ CancerExaminationImageAnnotation secondMarkerSameImage = annotation(1, 30, 40, 2);
+ CancerExaminationImageAnnotation markerOnOtherImage = annotation(2, 50, 60, 1);
+ when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationList(1L, 2L))
+ .thenReturn(Arrays.asList(firstMarker, secondMarkerSameImage, markerOnOtherImage));
+
+ ArrayList result = service
+ .getCancerExaminationImageAnnotationCasesheet(1L, 2L);
+
+ assertEquals(2, result.size());
+ assertEquals(2, result.get(0).getMarkers().size());
+ assertEquals(1, result.get(1).getMarkers().size());
+ }
+
+ @Test
+ void getCancerExaminationImageAnnotationCasesheet_returnsNothingWhenNoImageWasAnnotated() {
+ when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationList(1L, 2L))
+ .thenReturn(new ArrayList<>());
+ assertTrue(service.getCancerExaminationImageAnnotationCasesheet(1L, 2L).isEmpty());
+ }
+
+ private CancerExaminationImageAnnotation annotation(int imageId, int x, int y, int point) {
+ CancerExaminationImageAnnotation annotation = new CancerExaminationImageAnnotation();
+ annotation.setCancerImageID(imageId);
+ annotation.setxCoordinate(x);
+ annotation.setyCoordinate(y);
+ annotation.setPoint(point);
+ annotation.setPointDesc("marker");
+ return annotation;
+ }
+
+ @Test
+ void getBeneficiaryVisitDetails_mapsTheStoredVisitRow() {
+ when(benVisitDetailRepo.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(oneEmptyRow());
+ assertNotNull(service.getBeneficiaryVisitDetails(1L, 2L));
+
+ when(benVisitDetailRepo.getBeneficiaryVisitDetails(3L, 4L)).thenReturn(null);
+ assertNull(service.getBeneficiaryVisitDetails(3L, 4L));
+ }
+
+ @Test
+ void getBenNurseDataForCaseSheet_gathersEverySectionOfTheNurseCaseSheet() {
+ when(benVisitDetailRepo.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L)).thenReturn(new ArrayList<>());
+ when(cancerLymphNodeExaminationRepo.getBenCancerLymphNodeDetails(1L, 2L)).thenReturn(new ArrayList<>());
+
+ Map caseSheet = service.getBenNurseDataForCaseSheet(1L, 2L);
+
+ assertEquals(12, caseSheet.size());
+ assertTrue(caseSheet.containsKey("oralExamination"));
+ }
+
+ @Test
+ void theRemainingExaminationReadsDelegateToTheirRepositories() {
+ BenObstetricCancerHistory obstetric = new BenObstetricCancerHistory();
+ when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistory(1L, 2L)).thenReturn(obstetric);
+ assertEquals(obstetric, service.getBenObstetricDetailsData(1L, 2L));
+
+ BenCancerVitalDetail vital = new BenCancerVitalDetail();
+ when(benCancerVitalDetailRepo.getBenCancerVitalDetail(1L, 2L)).thenReturn(vital);
+ assertEquals(vital, service.getBenCancerVitalDetailData(1L, 2L));
+
+ CancerAbdominalExamination abdominal = new CancerAbdominalExamination();
+ when(cancerAbdominalExaminationRepo.getBenCancerAbdominalExaminationDetails(1L, 2L)).thenReturn(abdominal);
+ assertEquals(abdominal, service.getBenCancerAbdominalExaminationData(1L, 2L));
+
+ CancerBreastExamination breast = new CancerBreastExamination();
+ when(cancerBreastExaminationRepo.getBenCancerBreastExaminationDetails(1L, 2L)).thenReturn(breast);
+ assertEquals(breast, service.getBenCancerBreastExaminationData(1L, 2L));
+
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ when(cancerSignAndSymptomsRepo.getBenCancerSignAndSymptomsDetails(1L, 2L)).thenReturn(symptoms);
+ assertEquals(symptoms, service.getBenCancerSignAndSymptomsData(1L, 2L));
+
+ List lymphNodes = new ArrayList<>();
+ when(cancerLymphNodeExaminationRepo.getBenCancerLymphNodeDetails(1L, 2L)).thenReturn(lymphNodes);
+ assertEquals(lymphNodes, service.getBenCancerLymphNodeDetailsData(1L, 2L));
+
+ CancerOralExamination oral = new CancerOralExamination();
+ when(cancerOralExaminationRepo.getBenCancerOralExaminationDetails(1L, 2L)).thenReturn(oral);
+ assertEquals(oral, service.getBenCancerOralExaminationData(1L, 2L));
+ }
+ }
+
+ @Nested
+ @DisplayName("past visit history tables")
+ class HistoryTables {
+
+ @Test
+ void getBenCancerFamilyHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benFamilyCancerHistoryRepo.getBenCancerFamilyHistory(1L)).thenReturn(oneEmptyRow());
+ assertTrue(service.getBenCancerFamilyHistory(1L).contains("\"data\":[{"));
+
+ when(benFamilyCancerHistoryRepo.getBenCancerFamilyHistory(2L)).thenReturn(null);
+ assertTrue(service.getBenCancerFamilyHistory(2L).contains("\"data\":[]"));
+ }
+
+ @Test
+ void getBenCancerPersonalHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L)).thenReturn(oneEmptyRow());
+ assertTrue(service.getBenCancerPersonalHistory(1L).contains("\"data\":[{"));
+
+ when(benPersonalCancerHistoryRepo.getBenPersonalHistory(2L)).thenReturn(null);
+ assertTrue(service.getBenCancerPersonalHistory(2L).contains("\"data\":[]"));
+ }
+
+ @Test
+ void getBenCancerPersonalDietHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(1L)).thenReturn(oneEmptyRow());
+ assertTrue(service.getBenCancerPersonalDietHistory(1L).contains("\"data\":[{"));
+
+ when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(2L)).thenReturn(null);
+ assertTrue(service.getBenCancerPersonalDietHistory(2L).contains("\"data\":[]"));
+ }
+
+ @Test
+ void getBenCancerObstetricHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistoryData(1L)).thenReturn(oneEmptyRow());
+ assertTrue(service.getBenCancerObstetricHistory(1L).contains("\"data\":[{"));
+
+ when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistoryData(2L)).thenReturn(null);
+ assertTrue(service.getBenCancerObstetricHistory(2L).contains("\"data\":[]"));
+ }
+ }
+
+ @Nested
+ @DisplayName("examination saves")
+ class ExaminationSaves {
+
+ @Test
+ void saveLymphNodeDetails_stampsTheVisitOntoEveryNodeAndReturnsTheLastId() {
+ CancerLymphNodeDetails node = new CancerLymphNodeDetails();
+ CancerLymphNodeDetails stored = new CancerLymphNodeDetails();
+ stored.setID(9L);
+ List nodes = Collections.singletonList(node);
+ when(cancerLymphNodeExaminationRepo.saveAll(nodes)).thenReturn(Collections.singletonList(stored));
+
+ assertEquals(9L, service.saveLymphNodeDetails(nodes, 1L, 2L));
+ assertEquals(1L, node.getBenVisitID());
+ assertEquals(2L, node.getVisitCode());
+ }
+
+ @Test
+ void saveLymphNodeDetails_returnsNullWhenNoNodeWasStored() {
+ List nodes = Collections.singletonList(new CancerLymphNodeDetails());
+ when(cancerLymphNodeExaminationRepo.saveAll(nodes)).thenReturn(new ArrayList<>());
+ assertNull(service.saveLymphNodeDetails(nodes, 1L, 2L));
+ }
+
+ @Test
+ void saveCancerSignAndSymptomsData_stampsTheVisitBeforeSaving() {
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ CancerSignAndSymptoms stored = new CancerSignAndSymptoms();
+ stored.setID(10L);
+ when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(stored);
+
+ assertEquals(10L, service.saveCancerSignAndSymptomsData(symptoms, 1L, 2L));
+ assertEquals(1L, symptoms.getBenVisitID());
+ assertEquals(2L, symptoms.getVisitCode());
+
+ when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(null);
+ assertNull(service.saveCancerSignAndSymptomsData(symptoms));
+ }
+
+ @Test
+ void saveCancerOralExaminationData_flattensThePreMalignantLesionTypes() {
+ CancerOralExamination oral = new CancerOralExamination();
+ oral.setPreMalignantLesionTypeList(Arrays.asList("Leukoplakia", "Erythroplakia"));
+ CancerOralExamination stored = new CancerOralExamination();
+ stored.setID(11L);
+ when(cancerOralExaminationRepo.save(oral)).thenReturn(stored);
+
+ assertEquals(11L, service.saveCancerOralExaminationData(oral));
+ assertEquals("Leukoplakia,Erythroplakia,", oral.getPreMalignantLesionType());
+
+ when(cancerOralExaminationRepo.save(oral)).thenReturn(null);
+ assertNull(service.saveCancerOralExaminationData(oral));
+ }
+
+ @Test
+ void saveCancerBreastExaminationData_returnsTheStoredId() {
+ CancerBreastExamination breast = new CancerBreastExamination();
+ CancerBreastExamination stored = new CancerBreastExamination();
+ stored.setID(12L);
+ when(cancerBreastExaminationRepo.save(breast)).thenReturn(stored);
+ assertEquals(12L, service.saveCancerBreastExaminationData(breast));
+
+ when(cancerBreastExaminationRepo.save(breast)).thenReturn(null);
+ assertNull(service.saveCancerBreastExaminationData(breast));
+ }
+
+ @Test
+ void saveCancerAbdominalExaminationData_returnsTheStoredId() {
+ CancerAbdominalExamination abdominal = new CancerAbdominalExamination();
+ CancerAbdominalExamination stored = new CancerAbdominalExamination();
+ stored.setID(13L);
+ when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(stored);
+ assertEquals(13L, service.saveCancerAbdominalExaminationData(abdominal));
+
+ when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(null);
+ assertNull(service.saveCancerAbdominalExaminationData(abdominal));
+ }
+
+ @Test
+ void saveCancerGynecologicalExaminationData_flattensTheLesionTypes() {
+ CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination();
+ gynecological.setTypeOfLesionList(Arrays.asList("Polyp", "Ulcer"));
+ CancerGynecologicalExamination stored = new CancerGynecologicalExamination();
+ stored.setID(14L);
+ when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(stored);
+
+ assertEquals(14L, service.saveCancerGynecologicalExaminationData(gynecological));
+ assertEquals("Polyp,Ulcer,", gynecological.getTypeOfLesion());
+
+ when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(null);
+ assertNull(service.saveCancerGynecologicalExaminationData(gynecological));
+ }
+
+ @Test
+ void saveDocExaminationImageAnnotation_expandsEveryMarkerIntoItsOwnRow() {
+ WrapperCancerExamImgAnotasn wrapper = new WrapperCancerExamImgAnotasn();
+ wrapper.setBeneficiaryRegID(1L);
+ wrapper.setImageID(2);
+ ArrayList> markers = new ArrayList<>();
+ Map marker = new HashMap<>();
+ marker.put("xCord", 10d);
+ marker.put("yCord", 20d);
+ marker.put("point", 1d);
+ marker.put("description", "lesion");
+ markers.add(marker);
+ wrapper.setMarkers(markers);
+
+ when(cancerExaminationImageAnnotationRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.saveDocExaminationImageAnnotation(Collections.singletonList(wrapper), 3L, 4L));
+ assertEquals(3L, wrapper.getVisitID());
+ }
+
+ @Test
+ void saveDocExaminationImageAnnotation_returnsNullWhenNoMarkerWasDrawn() {
+ WrapperCancerExamImgAnotasn wrapper = new WrapperCancerExamImgAnotasn();
+ when(cancerExaminationImageAnnotationRepo.saveAll(any())).thenReturn(new ArrayList<>());
+ assertNull(service.saveDocExaminationImageAnnotation(Collections.singletonList(wrapper), 3L, 4L));
+ }
+
+ @Test
+ void getCancerExaminationImageAnnotationList_ignoresEmptyWrappers() {
+ assertTrue(service.getCancerExaminationImageAnnotationList(new ArrayList<>(), 1L).isEmpty());
+ assertTrue(service
+ .getCancerExaminationImageAnnotationList(Collections.singletonList(null), 1L).isEmpty());
+ }
+ }
+
+ @Nested
+ @DisplayName("examination updates")
+ class ExaminationUpdates {
+
+ @Test
+ void updateSignAndSymptomsExaminationDetails_updatesTheStoredRowWhenOneExists() {
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ when(cancerSignAndSymptomsRepo.getCancerSignAndSymptomsStatus(any(), any())).thenReturn("P");
+ when(cancerSignAndSymptomsRepo.updateCancerSignAndSymptoms(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateSignAndSymptomsExaminationDetails(symptoms));
+ }
+
+ @Test
+ void updateSignAndSymptomsExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ CancerSignAndSymptoms stored = new CancerSignAndSymptoms();
+ stored.setID(15L);
+ when(cancerSignAndSymptomsRepo.getCancerSignAndSymptomsStatus(any(), any())).thenReturn(null);
+ when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(stored);
+
+ assertEquals(1, service.updateSignAndSymptomsExaminationDetails(symptoms));
+ }
+
+ @Test
+ void updateLymphNodeExaminationDetails_clearsEveryNodeWhenTheEnlargementFlagIsTurnedOff() {
+ WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms();
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ symptoms.setLymphNode_Enlarged(false);
+ symptoms.setBeneficiaryRegID(1L);
+ symptoms.setVisitCode(2L);
+ wrapper.setCancerSignAndSymptoms(symptoms);
+ wrapper.setCancerLymphNodeDetails(new ArrayList<>());
+
+ when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatus(1L, 2L))
+ .thenReturn(statusRows(16L, "P"));
+ when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(16L, "U")).thenReturn(1);
+
+ assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper));
+ }
+
+ @Test
+ void updateLymphNodeExaminationDetails_replacesOnlyTheReportedNodesWhenTheFlagIsOn() {
+ WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms();
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ symptoms.setLymphNode_Enlarged(true);
+ symptoms.setBeneficiaryRegID(1L);
+ symptoms.setVisitCode(2L);
+ wrapper.setCancerSignAndSymptoms(symptoms);
+
+ CancerLymphNodeDetails measured = new CancerLymphNodeDetails();
+ measured.setLymphNodeName("Cervical");
+ measured.setSize_Left("2cm");
+ CancerLymphNodeDetails unmeasured = new CancerLymphNodeDetails();
+ unmeasured.setLymphNodeName("Cervical");
+ wrapper.setCancerLymphNodeDetails(Arrays.asList(measured, unmeasured));
+
+ when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatusForLymphnodeNameList(eq(1L), eq(2L),
+ any())).thenReturn(statusRows(17L, "N"));
+ when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(17L, "N")).thenReturn(1);
+ when(cancerLymphNodeExaminationRepo.saveAll(any()))
+ .thenReturn(new ArrayList<>(Collections.singletonList(measured)));
+
+ assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper));
+ }
+
+ @Test
+ void updateLymphNodeExaminationDetails_succeedsWhenNoNodeWasMeasured() {
+ WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms();
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ symptoms.setLymphNode_Enlarged(true);
+ wrapper.setCancerSignAndSymptoms(symptoms);
+ wrapper.setCancerLymphNodeDetails(new ArrayList<>());
+
+ assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper));
+ }
+
+ @Test
+ void updateLymphNodeExaminationDetails_reportsFailureWhenTheOldNodesCouldNotBeCleared() {
+ WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms();
+ CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms();
+ symptoms.setLymphNode_Enlarged(false);
+ wrapper.setCancerSignAndSymptoms(symptoms);
+ wrapper.setCancerLymphNodeDetails(new ArrayList<>());
+
+ when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatus(any(), any()))
+ .thenReturn(statusRows(18L, "N"));
+ when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(18L, "N")).thenReturn(0);
+
+ assertEquals(0, service.updateLymphNodeExaminationDetails(wrapper));
+ }
+
+ @Test
+ void updateCancerOralDetails_updatesTheStoredRowWhenOneExists() {
+ CancerOralExamination oral = new CancerOralExamination();
+ when(cancerOralExaminationRepo.getCancerOralExaminationStatus(any(), any())).thenReturn("P");
+ when(cancerOralExaminationRepo.updateCancerOralExaminationDetails(any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateCancerOralDetails(oral));
+ }
+
+ @Test
+ void updateCancerOralDetails_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerOralExamination oral = new CancerOralExamination();
+ CancerOralExamination stored = new CancerOralExamination();
+ stored.setID(19L);
+ when(cancerOralExaminationRepo.getCancerOralExaminationStatus(any(), any())).thenReturn(null);
+ when(cancerOralExaminationRepo.save(oral)).thenReturn(stored);
+
+ assertEquals(1, service.updateCancerOralDetails(oral));
+ }
+
+ @Test
+ void updateCancerBreastDetails_updatesTheStoredRowWhenOneExists() {
+ CancerBreastExamination breast = new CancerBreastExamination();
+ when(cancerBreastExaminationRepo.getCancerBreastExaminationStatus(any(), any())).thenReturn("P");
+ when(cancerBreastExaminationRepo.updateCancerBreastExaminatio(any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U")))
+ .thenReturn(1);
+
+ assertEquals(1, service.updateCancerBreastDetails(breast));
+ }
+
+ @Test
+ void updateCancerBreastDetails_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerBreastExamination breast = new CancerBreastExamination();
+ CancerBreastExamination stored = new CancerBreastExamination();
+ stored.setID(20L);
+ when(cancerBreastExaminationRepo.getCancerBreastExaminationStatus(any(), any())).thenReturn(null);
+ when(cancerBreastExaminationRepo.save(breast)).thenReturn(stored);
+
+ assertEquals(1, service.updateCancerBreastDetails(breast));
+ }
+
+ @Test
+ void updateCancerAbdominalExaminationDetails_updatesTheStoredRowWhenOneExists() {
+ CancerAbdominalExamination abdominal = new CancerAbdominalExamination();
+ when(cancerAbdominalExaminationRepo.getCancerAbdominalExaminationStatus(any(), any())).thenReturn("P");
+ when(cancerAbdominalExaminationRepo.updateCancerAbdominalExamination(any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U")))
+ .thenReturn(1);
+
+ assertEquals(1, service.updateCancerAbdominalExaminationDetails(abdominal));
+ }
+
+ @Test
+ void updateCancerAbdominalExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerAbdominalExamination abdominal = new CancerAbdominalExamination();
+ CancerAbdominalExamination stored = new CancerAbdominalExamination();
+ stored.setID(21L);
+ when(cancerAbdominalExaminationRepo.getCancerAbdominalExaminationStatus(any(), any())).thenReturn(null);
+ when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(stored);
+
+ assertEquals(1, service.updateCancerAbdominalExaminationDetails(abdominal));
+ }
+
+ @Test
+ void updateCancerGynecologicalExaminationDetails_updatesTheStoredRowWhenOneExists() {
+ CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination();
+ when(cancerGynecologicalExaminationRepo.getCancerGynecologicalExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(cancerGynecologicalExaminationRepo.updateCancerGynecologicalExamination(any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1);
+
+ assertEquals(1, service.updateCancerGynecologicalExaminationDetails(gynecological));
+ }
+
+ @Test
+ void updateCancerGynecologicalExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination();
+ CancerGynecologicalExamination stored = new CancerGynecologicalExamination();
+ stored.setID(22L);
+ when(cancerGynecologicalExaminationRepo.getCancerGynecologicalExaminationStatus(any(), any()))
+ .thenReturn(null);
+ when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(stored);
+
+ assertEquals(1, service.updateCancerGynecologicalExaminationDetails(gynecological));
+ }
+
+ @Test
+ void updateCancerExamImgAnotasnDetails_replacesTheAnnotationsOfEveryTouchedImage() {
+ CancerExaminationImageAnnotation complete = new CancerExaminationImageAnnotation();
+ complete.setCancerImageID(1);
+ complete.setBeneficiaryRegID(1L);
+ complete.setVisitCode(2L);
+ complete.setxCoordinate(10);
+ complete.setyCoordinate(20);
+ complete.setCreatedBy("nurse");
+ CancerExaminationImageAnnotation incomplete = new CancerExaminationImageAnnotation();
+ incomplete.setCancerImageID(1);
+ List annotations = Arrays.asList(complete, incomplete);
+
+ when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationDetailsStatus(eq(1L), eq(2L),
+ any())).thenReturn(statusRows(23L, "P"));
+ when(cancerExaminationImageAnnotationRepo.deleteExistingImageAnnotationDetails(23L, "U")).thenReturn(1);
+ when(cancerExaminationImageAnnotationRepo.saveAll(any()))
+ .thenReturn(new ArrayList<>(Collections.singletonList(complete)));
+
+ assertEquals(1, service.updateCancerExamImgAnotasnDetails(annotations));
+ assertEquals("nurse", complete.getModifiedBy());
+ }
+
+ @Test
+ void updateCancerExamImgAnotasnDetails_succeedsWhenNoImageWasAnnotated() {
+ assertEquals(1, service.updateCancerExamImgAnotasnDetails(new ArrayList<>()));
+ }
+
+ @Test
+ void updateCancerExamImgAnotasnDetails_reportsFailureWhenTheOldAnnotationsCouldNotBeCleared() {
+ CancerExaminationImageAnnotation annotation = new CancerExaminationImageAnnotation();
+ annotation.setCancerImageID(1);
+ List annotations = Collections.singletonList(annotation);
+
+ when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationDetailsStatus(any(), any(),
+ any())).thenReturn(statusRows(24L, "N"));
+ when(cancerExaminationImageAnnotationRepo.deleteExistingImageAnnotationDetails(24L, "N")).thenReturn(0);
+
+ assertEquals(0, service.updateCancerExamImgAnotasnDetails(annotations));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java
new file mode 100644
index 00000000..e3dac130
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java
@@ -0,0 +1,636 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.cancerScreening;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyShort;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus;
+import com.iemr.mmu.data.doctor.CancerDiagnosis;
+import com.iemr.mmu.data.nurse.BenCancerVitalDetail;
+import com.iemr.mmu.data.nurse.BeneficiaryVisitDetail;
+import com.iemr.mmu.data.nurse.CommonUtilityClass;
+import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo;
+import com.iemr.mmu.repo.registrar.RegistrarRepoBenData;
+import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl;
+import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl;
+import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl;
+import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl;
+
+class CSServiceImplTest {
+
+ @Mock
+ private CSNurseServiceImpl cSNurseServiceImpl;
+ @Mock
+ private CSDoctorServiceImpl cSDoctorServiceImpl;
+ @Mock
+ private CSOncologistServiceImpl csOncologistServiceImpl;
+ @Mock
+ private CommonNurseServiceImpl commonNurseServiceImpl;
+ @Mock
+ private CSCarestreamServiceImpl cSCarestreamServiceImpl;
+ @Mock
+ private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl;
+ @Mock
+ private TeleConsultationServiceImpl teleConsultationServiceImpl;
+ @Mock
+ private CommonDoctorServiceImpl commonDoctorServiceImpl;
+ @Mock
+ private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo;
+ @Mock
+ private RegistrarRepoBenData registrarRepoBenData;
+
+ @InjectMocks
+ private CSServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ private static JsonObject json(String raw) {
+ return JsonParser.parseString(raw).getAsJsonObject();
+ }
+
+ private static String visitDetailsBlock() {
+ return "\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"Screening\","
+ + "\"visitCategory\":\"Cancer Screening\"}";
+ }
+
+ @Nested
+ @DisplayName("saving nurse data")
+ class NurseSave {
+
+ @Test
+ void saveCancerScreeningNurseData_ignoresARequestWithoutVisitDetails() throws Exception {
+ assertNull(service.saveCancerScreeningNurseData(null, "auth"));
+ assertNull(service.saveCancerScreeningNurseData(json("{}"), "auth"));
+ }
+
+ @Test
+ void saveCancerScreeningNurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception {
+ when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus());
+
+ assertEquals(0L, service.saveCancerScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth"));
+ verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any());
+ }
+
+ @Test
+ void saveCancerScreeningNurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception {
+ when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1);
+
+ assertEquals(0L, service.saveCancerScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth"));
+ }
+
+ @Test
+ void saveCancerScreeningNurseData_sendsTheBeneficiaryToTheOncologistByDefault() throws Exception {
+ stubVisitCreation();
+ when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(1L);
+
+ String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{}}";
+
+ assertEquals(1L, service.saveCancerScreeningNurseData(json(request), "auth"));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 9),
+ org.mockito.ArgumentMatchers.eq((short) 0), org.mockito.ArgumentMatchers.eq((short) 0),
+ org.mockito.ArgumentMatchers.eq((short) 0), org.mockito.ArgumentMatchers.eq((short) 1), anyLong(),
+ any());
+ }
+
+ @Test
+ void saveCancerScreeningNurseData_sendsTheBeneficiaryToTheDoctorAndRadiologistWhenBothAreNeeded()
+ throws Exception {
+ stubVisitCreation();
+ when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L);
+ when(beneficiaryFlowStatusRepo.getBenDataForCareStream(any())).thenReturn(new ArrayList<>());
+ when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString()))
+ .thenReturn(1);
+
+ String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{},\"sendToDoctorWorklist\":true,"
+ + "\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1,\"referredToMammogram\":true}}}";
+
+ assertEquals(2L, service.saveCancerScreeningNurseData(json(request), "auth"));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 9),
+ org.mockito.ArgumentMatchers.eq((short) 1), org.mockito.ArgumentMatchers.eq((short) 0),
+ org.mockito.ArgumentMatchers.eq((short) 1), org.mockito.ArgumentMatchers.eq((short) 0), anyLong(),
+ any());
+ }
+
+ @Test
+ void saveCancerScreeningNurseData_leavesTheFlowUntouchedWhenASectionFailsToSave() throws Exception {
+ stubVisitCreation();
+ when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(null);
+
+ String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{}}";
+
+ assertNull(service.saveCancerScreeningNurseData(json(request), "auth"));
+ verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowNurseAfterNurseActivity(any(), anyLong(),
+ anyLong(), anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any());
+ }
+
+ private void stubVisitCreation() throws Exception {
+ when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0);
+ when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L);
+ when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L);
+ }
+
+ @Test
+ void saveBenVisitDetails_returnsNothingWhenAVisitWasAlreadyCreatedRecently() throws Exception {
+ when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1);
+ assertTrue(service.saveBenVisitDetails(new BeneficiaryVisitDetail(), new CommonUtilityClass()).isEmpty());
+ }
+ }
+
+ @Nested
+ @DisplayName("saving the individual nurse sections")
+ class NurseSections {
+
+ @Test
+ void saveBenHistoryDetails_treatsAnAbsentHistoryBlockAsAlreadyDone() throws Exception {
+ assertEquals(1L, service.saveBenHistoryDetails(json("{}"), 1L, 2L));
+ assertEquals(1L, service.saveBenHistoryDetails(null, 1L, 2L));
+ }
+
+ @Test
+ void saveBenHistoryDetails_savesEverySectionThatWasSent() throws Exception {
+ when(cSNurseServiceImpl.saveBenFamilyCancerHistory(any())).thenReturn(1);
+ when(cSNurseServiceImpl.saveBenPersonalCancerHistory(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveBenPersonalCancerDietHistory(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveBenObstetricCancerHistory(any())).thenReturn(1L);
+
+ String request = "{\"historyDetails\":{\"familyHistory\":{\"diseases\":[{\"diseaseType\":\"Breast\"}]},"
+ + "\"personalHistory\":{},\"pastObstetricHistory\":{}}}";
+
+ assertEquals(1L, service.saveBenHistoryDetails(json(request), 1L, 2L));
+ }
+
+ @Test
+ void saveBenHistoryDetails_treatsEachAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1L, service.saveBenHistoryDetails(json("{\"historyDetails\":{}}"), 1L, 2L));
+ }
+
+ @Test
+ void saveBenHistoryDetails_failsWhenTheFamilyHistoryCarriesNoDisease() {
+ // An empty disease array leaves the family-history flag unset, which the
+ // success check then dereferences.
+ String request = "{\"historyDetails\":{\"familyHistory\":{\"diseases\":[]}}}";
+ assertThrows(NullPointerException.class, () -> service.saveBenHistoryDetails(json(request), 1L, 2L));
+ }
+
+ @Test
+ void saveBenHistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception {
+ when(cSNurseServiceImpl.saveBenPersonalCancerHistory(any())).thenReturn(0L);
+
+ String request = "{\"historyDetails\":{\"personalHistory\":{}}}";
+ assertNull(service.saveBenHistoryDetails(json(request), 1L, 2L));
+ }
+
+ @Test
+ void saveBenVitalsDetails_savesTheVitalsWhenTheyWereSent() throws Exception {
+ when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(3L);
+
+ assertEquals(3L, service.saveBenVitalsDetails(json("{\"vitalsDetails\":{}}"), 1L, 2L));
+ assertEquals(1L, service.saveBenVitalsDetails(json("{}"), 1L, 2L));
+ assertEquals(1L, service.saveBenVitalsDetails(null, 1L, 2L));
+ }
+
+ @Test
+ void saveBenFamilyHistoryDetails_isNotWiredUpYet() {
+ assertNull(service.saveBenFamilyHistoryDetails());
+ }
+
+ @Test
+ void saveBenExaminationDetails_treatsAnAbsentExaminationBlockAsAlreadyDone() throws Exception {
+ assertEquals(1L, service.saveBenExaminationDetails(json("{}"), 1L, "auth", 2L, 3L));
+ assertEquals(1L, service.saveBenExaminationDetails(null, 1L, "auth", 2L, 3L));
+ }
+
+ @Test
+ void saveBenExaminationDetails_treatsEachAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1L, service.saveBenExaminationDetails(json("{\"examinationDetails\":{}}"), 1L, "auth", 2L,
+ 3L));
+ }
+
+ @Test
+ void saveBenExaminationDetails_savesEverySectionThatWasSent() throws Exception {
+ when(cSNurseServiceImpl.saveCancerSignAndSymptomsData(any(), anyLong(), anyLong())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveLymphNodeDetails(any(), anyLong(), anyLong())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveCancerOralExaminationData(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveCancerAbdominalExaminationData(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveCancerGynecologicalExaminationData(any())).thenReturn(1L);
+ when(cSNurseServiceImpl.saveDocExaminationImageAnnotation(any(), anyLong(), anyLong())).thenReturn(1L);
+
+ String request = "{\"examinationDetails\":{\"signsDetails\":{\"cancerSignAndSymptoms\":{},"
+ + "\"cancerLymphNodeDetails\":[{}]},\"oralDetails\":{},\"breastDetails\":{},"
+ + "\"abdominalDetails\":{},\"gynecologicalDetails\":{},\"imageCoordinates\":[{}]}}";
+
+ assertEquals(1L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L));
+ }
+
+ @Test
+ void saveBenExaminationDetails_raisesAMammogramOrderWhenTheBeneficiaryWasReferred() throws Exception {
+ when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L);
+ when(beneficiaryFlowStatusRepo.getBenDataForCareStream(3L)).thenReturn(new ArrayList<>());
+ when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString()))
+ .thenReturn(1);
+
+ String request = "{\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1,"
+ + "\"referredToMammogram\":true}}}";
+
+ assertEquals(2L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L));
+ }
+
+ @Test
+ void saveBenExaminationDetails_reportsAFailedMammogramOrder() throws Exception {
+ when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L);
+ when(beneficiaryFlowStatusRepo.getBenDataForCareStream(3L)).thenReturn(new ArrayList<>());
+ when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString()))
+ .thenReturn(0);
+
+ String request = "{\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1,"
+ + "\"referredToMammogram\":true}}}";
+
+ assertEquals(3L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L));
+ }
+
+ @Test
+ void saveBenExaminationDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception {
+ when(cSNurseServiceImpl.saveCancerOralExaminationData(any())).thenReturn(0L);
+
+ String request = "{\"examinationDetails\":{\"oralDetails\":{}}}";
+ assertNull(service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L));
+ }
+ }
+
+ @Nested
+ @DisplayName("updating nurse data from the doctor screen")
+ class NurseUpdates {
+
+ @Test
+ void UpdateCSHistoryNurseData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1, service.UpdateCSHistoryNurseData(json("{}")));
+ assertEquals(1, service.UpdateCSHistoryNurseData(null));
+ }
+
+ @Test
+ void UpdateCSHistoryNurseData_updatesEverySectionThatWasSent() throws Exception {
+ when(cSNurseServiceImpl.updateBeneficiaryFamilyCancerHistory(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateBenObstetricCancerHistory(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateBenPersonalCancerHistory(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateBenPersonalCancerDietHistory(any())).thenReturn(1);
+
+ String request = "{\"familyHistory\":[{}],\"pastObstetricHistory\":{},\"personalHistory\":{}}";
+
+ assertEquals(1, service.UpdateCSHistoryNurseData(json(request)));
+ }
+
+ @Test
+ void UpdateCSHistoryNurseData_treatsAnEmptyFamilyHistoryAsAlreadyDone() throws Exception {
+ assertEquals(1, service.UpdateCSHistoryNurseData(json("{\"familyHistory\":[]}")));
+ }
+
+ @Test
+ void UpdateCSHistoryNurseData_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception {
+ when(cSNurseServiceImpl.updateBenObstetricCancerHistory(any())).thenReturn(0);
+ assertEquals(0, service.UpdateCSHistoryNurseData(json("{\"pastObstetricHistory\":{}}")));
+ }
+
+ @Test
+ void updateBenExaminationDetail_treatsEveryAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1, service.updateBenExaminationDetail(json("{}")));
+ assertEquals(1, service.updateBenExaminationDetail(null));
+ }
+
+ @Test
+ void updateBenExaminationDetail_updatesEverySectionThatWasSent() throws Exception {
+ when(cSNurseServiceImpl.updateSignAndSymptomsExaminationDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateLymphNodeExaminationDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateCancerOralDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateCancerBreastDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateCancerAbdominalExaminationDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.updateCancerGynecologicalExaminationDetails(any())).thenReturn(1);
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationList(any(), any()))
+ .thenReturn(new ArrayList<>());
+ when(cSNurseServiceImpl.updateCancerExamImgAnotasnDetails(any())).thenReturn(1);
+
+ String request = "{\"visitCode\":3,\"signsDetails\":{\"cancerSignAndSymptoms\":{},"
+ + "\"cancerLymphNodeDetails\":[{}]},\"oralDetails\":{},\"breastDetails\":{},"
+ + "\"abdominalDetails\":{},\"gynecologicalDetails\":{\"fileIDs\":[\"a\",\"b\"]},"
+ + "\"imageCoordinates\":[{}]}";
+
+ assertEquals(1, service.updateBenExaminationDetail(json(request)));
+ }
+
+ @Test
+ void updateBenExaminationDetail_treatsAnEmptySignsBlockAsAlreadyDone() throws Exception {
+ assertEquals(1, service.updateBenExaminationDetail(json("{\"signsDetails\":{}}")));
+ }
+
+ @Test
+ void updateBenExaminationDetail_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception {
+ when(cSNurseServiceImpl.updateCancerOralDetails(any())).thenReturn(0);
+ assertEquals(0, service.updateBenExaminationDetail(json("{\"oralDetails\":{}}")));
+ }
+
+ @Test
+ void updateBenVitalDetail_delegatesToTheNurseService() {
+ BenCancerVitalDetail vital = new BenCancerVitalDetail();
+ when(cSNurseServiceImpl.updateBenVitalDetail(vital)).thenReturn(1);
+ assertEquals(1, service.updateBenVitalDetail(vital));
+ }
+ }
+
+ @Nested
+ @DisplayName("reading the nurse and doctor screens")
+ class Reads {
+
+ /** One left-panel row, wide enough for the beneficiary summary mapper. */
+ private ArrayList oneBenDetailRow() {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[40]);
+ return rows;
+ }
+
+ @Test
+ void getBenDataFrmNurseToDocVisitDetailsScreen_returnsTheStoredVisit() throws Exception {
+ BeneficiaryVisitDetail visit = new BeneficiaryVisitDetail();
+ visit.setBenVisitID(1L);
+ when(commonNurseServiceImpl.getCSVisitDetails(1L, 2L)).thenReturn(visit);
+
+ assertTrue(service.getBenDataFrmNurseToDocVisitDetailsScreen(1L, 2L).contains("benVisitDetails"));
+
+ when(commonNurseServiceImpl.getCSVisitDetails(3L, 4L)).thenReturn(null);
+ assertEquals("{}", service.getBenDataFrmNurseToDocVisitDetailsScreen(3L, 4L));
+ }
+
+ @Test
+ void getBenDataFrmNurseToDocHistoryScreen_gathersEveryHistorySection() {
+ assertTrue(service.getBenDataFrmNurseToDocHistoryScreen(1L, 2L).contains("benFamilyHistory"));
+ verify(cSNurseServiceImpl).getBenPersonalCancerDietHistoryData(1L, 2L);
+ }
+
+ @Test
+ void getBenDataFrmNurseToDocVitalScreen_gathersTheVitalsAndTheirTrend() {
+ when(commonNurseServiceImpl.getGraphicalTrendData(1L, "cancer screening")).thenReturn(new HashMap<>());
+ assertTrue(service.getBenDataFrmNurseToDocVitalScreen(1L, 2L).contains("GraphData"));
+ }
+
+ @Test
+ void getBenDataFrmNurseToDocExaminationScreen_gathersEveryExaminedSystem() {
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L))
+ .thenReturn(new ArrayList<>());
+
+ assertTrue(service.getBenDataFrmNurseToDocExaminationScreen(1L, 2L).contains("imageCoordinates"));
+ verify(cSNurseServiceImpl).getBenCancerOralExaminationData(1L, 2L);
+ }
+
+ @Test
+ void getBenNurseDataForCaseSheet_gathersEverySectionOfTheNurseCaseSheet() {
+ when(cSNurseServiceImpl.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(new BeneficiaryVisitDetail());
+
+ assertTrue(service.getBenNurseDataForCaseSheet(1L, 2L).contains("benVisitDetail"));
+ verify(cSNurseServiceImpl).getBenCancerLymphNodeDetailsData(1L, 2L);
+ }
+
+ @Test
+ void getBenDataForCaseSheet_combinesTheNurseAndDoctorSections() throws Exception {
+ when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>());
+ when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>());
+ when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(oneBenDetailRow());
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L))
+ .thenReturn(new ArrayList<>());
+
+ assertTrue(service.getBenDataForCaseSheet(3L, 1L, 2L, "auth").contains("ImageAnnotatedData"));
+ }
+
+ @Test
+ void getCancerCasesheetData_readsTheBeneficiaryKeysOutOfTheRequest() throws Exception {
+ when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>());
+ when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>());
+ when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(oneBenDetailRow());
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L))
+ .thenReturn(new ArrayList<>());
+
+ JSONObject request = new JSONObject();
+ request.put("benRegID", 1L);
+ request.put("benVisitID", 4L);
+ request.put("benFlowID", 3L);
+ request.put("visitCode", 2L);
+
+ assertTrue(service.getCancerCasesheetData(request, "auth").contains("BeneficiaryData"));
+ }
+
+ @Test
+ void getCancerCasesheetData_returnsNothingForAnEmptyRequest() throws Exception {
+ assertNull(service.getCancerCasesheetData(new JSONObject(), "auth"));
+ }
+
+ @Test
+ void getCancerCasesheetData_tolratesARequestMissingTheBeneficiaryKeys() throws Exception {
+ when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(null, null)).thenReturn(new HashMap<>());
+ when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(null, null)).thenReturn(new HashMap<>());
+ when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(null, null)).thenReturn(oneBenDetailRow());
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(null, null))
+ .thenReturn(new ArrayList<>());
+
+ JSONObject request = new JSONObject();
+ request.put("unrelated", 1);
+ request.put("alsoUnrelated", 2);
+
+ assertTrue(service.getCancerCasesheetData(request, "auth").contains("BeneficiaryData"));
+ }
+
+ @Test
+ void thePastHistoryReadsDelegateToTheNurseService() {
+ when(cSNurseServiceImpl.getBenCancerFamilyHistory(1L)).thenReturn("family");
+ assertEquals("family", service.getBenFamilyHistoryData(1L));
+
+ when(cSNurseServiceImpl.getBenCancerPersonalHistory(1L)).thenReturn("personal");
+ assertEquals("personal", service.getBenPersonalHistoryData(1L));
+
+ when(cSNurseServiceImpl.getBenCancerPersonalDietHistory(1L)).thenReturn("diet");
+ assertEquals("diet", service.getBenPersonalDietHistoryData(1L));
+
+ when(cSNurseServiceImpl.getBenCancerObstetricHistory(1L)).thenReturn("obstetric");
+ assertEquals("obstetric", service.getBenObstetricHistoryData(1L));
+ }
+
+ @Test
+ void theDoctorDiagnosisReadsDelegateToTheDoctorService() {
+ when(cSDoctorServiceImpl.getBenCancerDiagnosisData(1L, 2L)).thenReturn(new CancerDiagnosis());
+
+ assertTrue(service.getBenDoctorDiagnosisData(1L, 2L).contains("benDiagnosisDetails"));
+ assertTrue(service.getBenCaseRecordFromDoctorCS(1L, 2L).contains("diagnosis"));
+ }
+ }
+
+ @Nested
+ @DisplayName("saving and updating doctor data")
+ class DoctorData {
+
+ private String doctorRequest(String extra) {
+ return "{\"diagnosis\":{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"benFlowID\":4,"
+ + "\"createdBy\":\"doctor\"" + extra + "}}";
+ }
+
+ @Test
+ void saveCancerScreeningDoctorData_savesTheDiagnosisAndAdvancesTheFlow() throws Exception {
+ when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(1L);
+
+ assertEquals(1L, service.saveCancerScreeningDoctorData(json(doctorRequest("")), "auth"));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any());
+ }
+
+ @Test
+ void saveCancerScreeningDoctorData_ignoresARequestWithoutADiagnosis() throws Exception {
+ assertNull(service.saveCancerScreeningDoctorData(json("{}"), "auth"));
+ }
+
+ @Test
+ void saveCancerScreeningDoctorData_leavesTheFlowUntouchedWhenTheDiagnosisWasNotStored() throws Exception {
+ when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(0L);
+
+ assertNull(service.saveCancerScreeningDoctorData(json(doctorRequest("")), "auth"));
+ verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowAfterDocData(any(), any(), any(), any(),
+ anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any());
+ }
+
+ @Test
+ void saveCancerScreeningDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest()
+ throws Exception {
+ when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(1L);
+ when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1);
+ when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1);
+
+ String request = "{\"diagnosis\":{\"beneficiaryRegID\":1,\"serviceID\":4,\"createdBy\":\"doctor\"},"
+ + "\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\",\"fromTime\":\"10:00:00\","
+ + "\"toTime\":\"10:30:00\"}}";
+
+ assertEquals(1L, service.saveCancerScreeningDoctorData(json(request), "auth"));
+ verify(teleConsultationServiceImpl).createTCRequest(any());
+ }
+
+ @Test
+ void saveCancerScreeningDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception {
+ when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0);
+
+ String request = "{\"diagnosis\":{\"serviceID\":4,\"createdBy\":\"doctor\"},"
+ + "\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\",\"fromTime\":\"10:00:00\","
+ + "\"toTime\":\"10:30:00\"}}";
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.saveCancerScreeningDoctorData(json(request), "auth"));
+ assertEquals("Error while booking slot.", thrown.getMessage());
+ }
+
+ @Test
+ void saveBenDiagnosisDetails_storesTheDiagnosisWhenOneWasSent() throws Exception {
+ when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(4L);
+
+ assertEquals(4L, service.saveBenDiagnosisDetails(json(doctorRequest(""))));
+ assertEquals(1L, service.saveBenDiagnosisDetails(json("{}")));
+ assertEquals(1L, service.saveBenDiagnosisDetails(null));
+ }
+
+ @Test
+ void saveBenDiagnosisDetails_reportsFailureWhenTheDiagnosisWasNotStored() throws Exception {
+ when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(0L);
+ assertNull(service.saveBenDiagnosisDetails(json(doctorRequest(""))));
+ }
+
+ @Test
+ void updateCancerScreeningDoctorData_updatesTheDiagnosisAndAdvancesTheFlow() throws Exception {
+ when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(1);
+ when(beneficiaryFlowStatusRepo.updateBenFlowAfterTCSpcialistDoneForCanceScreening(4L, 1L, 3L))
+ .thenReturn(1);
+
+ assertEquals(1, service.updateCancerScreeningDoctorData(json(doctorRequest(""))));
+ }
+
+ @Test
+ void updateCancerScreeningDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception {
+ when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(1);
+ when(beneficiaryFlowStatusRepo.updateBenFlowAfterTCSpcialistDoneForCanceScreening(any(), any(), any()))
+ .thenReturn(0);
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.updateCancerScreeningDoctorData(json(doctorRequest(""))));
+ assertTrue(thrown.getMessage().contains("beneficiary flow status"));
+ }
+
+ @Test
+ void updateCancerScreeningDoctorData_failsWhenTheDiagnosisCouldNotBeUpdated() throws Exception {
+ when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(0);
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.updateCancerScreeningDoctorData(json(doctorRequest(""))));
+ assertEquals("Error while saving data.", thrown.getMessage());
+ }
+
+ @Test
+ void updateCancerScreeningDoctorData_rejectsARequestWithoutADiagnosis() {
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.updateCancerScreeningDoctorData(json("{}")));
+ assertEquals("Invalid request.", thrown.getMessage());
+
+ RuntimeException nullRequest = assertThrows(RuntimeException.class,
+ () -> service.updateCancerScreeningDoctorData(null));
+ assertEquals("Invalid request as it is null.", nullRequest.getMessage());
+ }
+
+ @Test
+ void updateCancerDiagnosisDetailsByOncologist_delegatesToTheOncologistService() {
+ CancerDiagnosis diagnosis = new CancerDiagnosis();
+ when(csOncologistServiceImpl.updateCancerDiagnosisDetailsByOncologist(diagnosis)).thenReturn(1);
+ assertEquals(1, service.updateCancerDiagnosisDetailsByOncologist(diagnosis));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java
new file mode 100644
index 00000000..e22e4c1a
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java
@@ -0,0 +1,235 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.cancerScreening;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.when;
+
+import java.sql.Date;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.MockedConstruction;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
+
+import com.iemr.mmu.data.doctor.CancerDiagnosis;
+import com.iemr.mmu.repo.doctor.CancerDiagnosisRepo;
+import com.iemr.mmu.utils.CookieUtil;
+
+class CancerDoctorAndCarestreamServiceTest {
+
+ @Nested
+ @DisplayName("CSDoctorServiceImpl")
+ class DoctorService {
+
+ @Mock
+ private CancerDiagnosisRepo cancerDiagnosisRepo;
+
+ @InjectMocks
+ private CSDoctorServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ private CancerDiagnosis diagnosisWithServices(String... services) {
+ CancerDiagnosis diagnosis = new CancerDiagnosis();
+ diagnosis.setBeneficiaryRegID(1L);
+ diagnosis.setVisitCode(2L);
+ diagnosis.setRefrredToAdditionalServiceList(Arrays.asList(services));
+ return diagnosis;
+ }
+
+ @Test
+ void saveCancerDiagnosisData_flattensTheAdditionalServicesBeforeSaving() {
+ CancerDiagnosis diagnosis = diagnosisWithServices("Radiology", "Oncology");
+ CancerDiagnosis stored = new CancerDiagnosis();
+ stored.setID(5L);
+ when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(stored);
+
+ assertEquals(5L, service.saveCancerDiagnosisData(diagnosis));
+ assertEquals("Radiology,Oncology", diagnosis.getRefrredToAdditionalService());
+ assertEquals(5L, stored.getVanSerialNo());
+ }
+
+ @Test
+ void saveCancerDiagnosisData_storesAnEmptyServiceListAsAnEmptyString() {
+ CancerDiagnosis diagnosis = new CancerDiagnosis();
+ when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(null);
+
+ assertNull(service.saveCancerDiagnosisData(diagnosis));
+ assertEquals("", diagnosis.getRefrredToAdditionalService());
+ }
+
+ @Test
+ void getBenCancerDiagnosisData_splitsTheStoredServicesBackIntoAList() {
+ CancerDiagnosis stored = new CancerDiagnosis();
+ stored.setRefrredToAdditionalService("Radiology,Oncology");
+ when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored);
+
+ assertEquals(List.of("Radiology", "Oncology"),
+ service.getBenCancerDiagnosisData(1L, 2L).getRefrredToAdditionalServiceList());
+ }
+
+ @Test
+ void getBenCancerDiagnosisData_namesTheReferredInstituteWhenOneIsLinked() {
+ CancerDiagnosis stored = new CancerDiagnosis();
+ com.iemr.mmu.data.institution.Institute institute = new com.iemr.mmu.data.institution.Institute();
+ institute.setInstitutionName("District Hospital");
+ stored.setInstitute(institute);
+ when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored);
+
+ assertEquals("District Hospital", service.getBenCancerDiagnosisData(1L, 2L).getReferredToInstituteName());
+ }
+
+ @Test
+ void getBenCancerDiagnosisData_returnsNothingForABeneficiaryWithNoDiagnosis() {
+ when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(null);
+
+ assertNull(service.getBenCancerDiagnosisData(1L, 2L));
+ }
+
+ @Test
+ void getBenDoctorEnteredDataForCaseSheet_carriesTheDiagnosis() {
+ CancerDiagnosis stored = new CancerDiagnosis();
+ when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored);
+
+ assertEquals(stored, service.getBenDoctorEnteredDataForCaseSheet(1L, 2L).get("diagnosis"));
+ }
+
+ @Test
+ void updateCancerDiagnosisDetailsByDoctor_updatesTheStoredRowWhenOneExists() {
+ CancerDiagnosis diagnosis = diagnosisWithServices("Radiology");
+ when(cancerDiagnosisRepo.getCancerDiagnosisStatuses(1L, 2L)).thenReturn("P");
+ when(cancerDiagnosisRepo.updateCancerDiagnosisDetailsByDoctor(any(), any(), any(), anyString(), any(),
+ any(), any(), eq("U"), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updateCancerDiagnosisDetailsByDoctor(diagnosis));
+ }
+
+ @Test
+ void updateCancerDiagnosisDetailsByDoctor_insertsAFreshRowWhenNoneIsStoredYet() {
+ CancerDiagnosis diagnosis = diagnosisWithServices("Radiology");
+ CancerDiagnosis stored = new CancerDiagnosis();
+ stored.setID(5L);
+ when(cancerDiagnosisRepo.getCancerDiagnosisStatuses(1L, 2L)).thenReturn(null);
+ when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(stored);
+
+ assertEquals(1, service.updateCancerDiagnosisDetailsByDoctor(diagnosis));
+
+ stored.setID(0L);
+ assertEquals(0, service.updateCancerDiagnosisDetailsByDoctor(diagnosis));
+ }
+ }
+
+ @Nested
+ @DisplayName("CSCarestreamServiceImpl")
+ class CarestreamService {
+
+ @Mock
+ private CookieUtil cookieUtil;
+
+ @InjectMocks
+ private CSCarestreamServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "carestreamOrderCreateURL", "http://carestream/order");
+ }
+
+ /** One beneficiary row as the flow-status query returns it. */
+ private ArrayList beneficiary(String name, short genderID) {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[] { name, "Devi", Date.valueOf("1990-05-04"), genderID });
+ return rows;
+ }
+
+ private MockedConstruction carestreamAnswering(String body) {
+ return mockConstruction(RestTemplate.class,
+ (mock, context) -> when(
+ mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)));
+ }
+
+ @Test
+ void createMamographyRequest_reportsAnAcceptedOrder() {
+ try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) {
+ assertEquals(1, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth"));
+ }
+ }
+
+ @Test
+ void createMamographyRequest_reportsARejectedOrder() {
+ try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":500}")) {
+ assertEquals(0, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth"));
+ }
+ }
+
+ @Test
+ void createMamographyRequest_mapsEveryGenderCodeCarestreamExpects() {
+ try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) {
+ assertEquals(1, service.createMamographyRequest(beneficiary("Ram", (short) 1), 1L, 2L, "auth"));
+ assertEquals(1, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth"));
+ assertEquals(1, service.createMamographyRequest(beneficiary("Kiran", (short) 3), 1L, 2L, "auth"));
+ }
+ }
+
+ @Test
+ void createMamographyRequest_sendsAnEmptyOrderForABeneficiaryWithNoDetails() {
+ try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) {
+ assertEquals(1, service.createMamographyRequest(new ArrayList<>(), 1L, 2L, "auth"));
+ }
+ }
+
+ @Test
+ void createMamographyRequest_reportsAnOrderThatCouldNotBeSent() {
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class,
+ (mock, context) -> when(
+ mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenThrow(new RuntimeException("carestream unreachable")))) {
+
+ assertEquals(0, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth"));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java
new file mode 100644
index 00000000..cf17108c
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java
@@ -0,0 +1,627 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.common.transaction;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyShort;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.iemr.mmu.data.anc.WrapperAncFindings;
+import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus;
+import com.iemr.mmu.data.login.Users;
+import com.iemr.mmu.data.nurse.CommonUtilityClass;
+import com.iemr.mmu.data.quickConsultation.BenChiefComplaint;
+import com.iemr.mmu.data.quickConsultation.BenClinicalObservations;
+import com.iemr.mmu.data.snomedct.SCTDescription;
+import com.iemr.mmu.data.tele_consultation.TeleconsultationRequestOBJ;
+import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo;
+import com.iemr.mmu.repo.doctor.BenReferDetailsRepo;
+import com.iemr.mmu.repo.doctor.DocWorkListRepo;
+import com.iemr.mmu.repo.login.UserLoginRepo;
+import com.iemr.mmu.repo.nurse.BenVisitDetailRepo;
+import com.iemr.mmu.repo.quickConsultation.BenChiefComplaintRepo;
+import com.iemr.mmu.repo.quickConsultation.BenClinicalObservationsRepo;
+import com.iemr.mmu.repo.quickConsultation.LabTestOrderDetailRepo;
+import com.iemr.mmu.repo.quickConsultation.PrescribedDrugDetailRepo;
+import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo;
+import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl;
+import com.iemr.mmu.service.snomedct.SnomedServiceImpl;
+import com.iemr.mmu.utils.CookieUtil;
+
+class CommonDoctorServiceImplTest {
+
+ @Mock
+ private BenClinicalObservationsRepo benClinicalObservationsRepo;
+ @Mock
+ private BenChiefComplaintRepo benChiefComplaintRepo;
+ @Mock
+ private DocWorkListRepo docWorkListRepo;
+ @Mock
+ private BenReferDetailsRepo benReferDetailsRepo;
+ @Mock
+ private LabTestOrderDetailRepo labTestOrderDetailRepo;
+ @Mock
+ private PrescribedDrugDetailRepo prescribedDrugDetailRepo;
+ @Mock
+ private PrescriptionDetailRepo prescriptionDetailRepo;
+ @Mock
+ private SnomedServiceImpl snomedServiceImpl;
+ @Mock
+ private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl;
+ @Mock
+ private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo;
+ @Mock
+ private CookieUtil cookieUtil;
+ @Mock
+ private BenVisitDetailRepo benVisitDetailRepo;
+ @Mock
+ private UserLoginRepo userLoginRepo;
+
+ @InjectMocks
+ private CommonDoctorServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "tmReferCheckValue", "Tele-consultation");
+ }
+
+ private static JsonObject json(String raw) {
+ return JsonParser.parseString(raw).getAsJsonObject();
+ }
+
+ private static WrapperAncFindings findings(BenChiefComplaint... complaints) {
+ ArrayList list = new ArrayList<>();
+ Collections.addAll(list, complaints);
+ return new WrapperAncFindings(1L, 2L, 4, "Alert", "Fever", "Anaemia", list, Boolean.FALSE, 3L);
+ }
+
+ private static BenChiefComplaint complaint(String name) {
+ BenChiefComplaint complaint = new BenChiefComplaint();
+ complaint.setChiefComplaint(name);
+ complaint.setBenChiefComplaintID(9L);
+ return complaint;
+ }
+
+ @Nested
+ @DisplayName("findings")
+ class Findings {
+
+ @Test
+ void saveFindings_reportsWhetherTheObservationWasStored() throws Exception {
+ BenClinicalObservations stored = new BenClinicalObservations();
+ when(benClinicalObservationsRepo.save(any())).thenReturn(stored);
+ assertEquals(1, service.saveFindings(json("{}")));
+
+ when(benClinicalObservationsRepo.save(any())).thenReturn(null);
+ assertEquals(0, service.saveFindings(json("{}")));
+ }
+
+ @Test
+ void saveDocFindings_storesTheObservationAndEveryNamedComplaint() {
+ when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations());
+ when(benChiefComplaintRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ WrapperAncFindings wrapper = findings(complaint("Fever"), new BenChiefComplaint());
+
+ assertEquals(1, service.saveDocFindings(wrapper));
+ verify(benChiefComplaintRepo).updateVanSerialNo(9L);
+ }
+
+ @Test
+ void saveDocFindings_succeedsWhenNoComplaintWasNamed() {
+ when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations());
+ assertEquals(1, service.saveDocFindings(findings()));
+ verify(benChiefComplaintRepo, never()).saveAll(any());
+ }
+
+ @Test
+ void saveDocFindings_reportsFailureWhenTheObservationWasNotStored() {
+ when(benClinicalObservationsRepo.save(any())).thenReturn(null);
+ assertEquals(0, service.saveDocFindings(findings()));
+ }
+
+ @Test
+ void saveDocFindings_reportsFailureWhenNotEveryComplaintWasStored() {
+ when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations());
+ when(benChiefComplaintRepo.saveAll(any())).thenReturn(new ArrayList<>());
+
+ assertEquals(0, service.saveDocFindings(findings(complaint("Fever"))));
+ }
+
+ @Test
+ void getSnomedCTcode_looksUpEverySymptomAndFallsBackWhenOneIsUnknown() {
+ SCTDescription known = new SCTDescription();
+ known.setConceptID("111");
+ known.setTerm("Fever");
+ when(snomedServiceImpl.findSnomedCTRecordFromTerm("Fever")).thenReturn(known);
+ when(snomedServiceImpl.findSnomedCTRecordFromTerm("Unknown")).thenReturn(null);
+
+ assertArrayEquals(new String[] { "111,N/A", "Fever,N/A" },
+ service.getSnomedCTcode("Fever, Unknown"));
+ }
+
+ @Test
+ void getSnomedCTcode_returnsEmptyCodesForNoSymptoms() {
+ assertArrayEquals(new String[] { "", "" }, service.getSnomedCTcode(null));
+ assertArrayEquals(new String[] { "", "" }, service.getSnomedCTcode(""));
+ }
+
+ @Test
+ void fetchBenPreviousSignificantFindings_mapsEveryStoredFinding() {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[] { new java.sql.Date(System.currentTimeMillis()), "Anaemia" });
+ when(benClinicalObservationsRepo.getPreviousSignificantFindings(1L)).thenReturn(rows);
+
+ assertTrue(service.fetchBenPreviousSignificantFindings(1L).contains("Anaemia"));
+ }
+
+ @Test
+ void getFindingsDetails_combinesTheObservationsAndComplaints() {
+ when(benClinicalObservationsRepo.getFindingsData(1L, 2L)).thenReturn(new ArrayList<>());
+ when(benChiefComplaintRepo.getBenChiefComplaints(1L, 2L)).thenReturn(new ArrayList<>());
+
+ assertNotNull(service.getFindingsDetails(1L, 2L));
+ }
+
+ @Test
+ void updateDocFindings_updatesTheObservationAndEveryNamedComplaint() {
+ when(benClinicalObservationsRepo.getBenClinicalObservationStatus(1L, 3L)).thenReturn("P");
+ when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(),
+ any(), eq("U"), anyLong(), anyLong())).thenReturn(1);
+ when(benChiefComplaintRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1, service.updateDocFindings(findings(complaint("Fever"))));
+ }
+
+ @Test
+ void updateDocFindings_succeedsWhenNoComplaintWasNamed() {
+ when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn("N");
+ when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(),
+ any(), eq("N"), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updateDocFindings(findings()));
+ }
+
+ @Test
+ void updateDocFindings_reportsFailureWhenTheObservationCouldNotBeUpdated() {
+ when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn("N");
+ when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(),
+ any(), any(), anyLong(), anyLong())).thenReturn(0);
+
+ assertEquals(0, service.updateDocFindings(findings()));
+ }
+
+ @Test
+ void updateBenClinicalObservations_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenClinicalObservations observations = new BenClinicalObservations();
+ BenClinicalObservations stored = new BenClinicalObservations();
+ stored.setClinicalObservationID(5L);
+ when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn(null);
+ when(benClinicalObservationsRepo.save(observations)).thenReturn(stored);
+
+ assertEquals(1, service.updateBenClinicalObservations(observations));
+ }
+
+ @Test
+ void updateBenClinicalObservations_reportsFailureWhenTheFreshRowWasNotStored() {
+ BenClinicalObservations observations = new BenClinicalObservations();
+ BenClinicalObservations stored = new BenClinicalObservations();
+ stored.setClinicalObservationID(0L);
+ when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn(null);
+ when(benClinicalObservationsRepo.save(observations)).thenReturn(stored);
+
+ assertEquals(0, service.updateBenClinicalObservations(observations));
+ assertEquals(0, service.updateBenClinicalObservations(null));
+ }
+
+ @Test
+ void updateDoctorBenChiefComplaints_succeedsWhenThereIsNothingToUpdate() {
+ assertEquals(1, service.updateDoctorBenChiefComplaints(null));
+ assertEquals(1, service.updateDoctorBenChiefComplaints(new ArrayList<>()));
+ }
+
+ @Test
+ void updateDoctorBenChiefComplaints_reportsNothingSavedWhenTheStoreDropsTheRows() {
+ List complaints = Collections.singletonList(complaint("Fever"));
+ when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(new ArrayList<>());
+
+ assertEquals(0, service.updateDoctorBenChiefComplaints(complaints));
+ }
+ }
+
+ @Nested
+ @DisplayName("work lists")
+ class WorkLists {
+
+ @Test
+ void getDocWorkList_serialisesTheStoredWorklist() {
+ when(docWorkListRepo.getDocWorkList()).thenReturn(new ArrayList<>());
+ assertNotNull(service.getDocWorkList());
+ }
+
+ @Test
+ void getDocWorkListNew_readsTheMmuWorklistWithinTheConfiguredWindow() {
+ ReflectionTestUtils.setField(service, "docWL", 10);
+ when(beneficiaryFlowStatusRepo.getDocWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getDocWorkListNew(1, 2, 3));
+ }
+
+ @Test
+ void getDocWorkListNew_readsTheTeleconsultationWorklist() {
+ when(beneficiaryFlowStatusRepo.getDocWorkListNewTC(1)).thenReturn(new ArrayList<>());
+ assertEquals("[]", service.getDocWorkListNew(1, 4, 3));
+ }
+
+ @Test
+ void getDocWorkListNew_returnsNothingForAnUnknownService() {
+ assertEquals("[]", service.getDocWorkListNew(1, 9, 3));
+ }
+
+ @Test
+ void getDocWorkListNewFutureScheduledForTM_onlyAppliesToTeleconsultation() {
+ when(beneficiaryFlowStatusRepo.getDocWorkListNewFutureScheduledTC(1)).thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getDocWorkListNewFutureScheduledForTM(1, 4));
+ assertEquals("[]", service.getDocWorkListNewFutureScheduledForTM(1, 2));
+ }
+
+ @Test
+ void getTCSpecialistWorkListNewForTM_onlyAppliesToTeleconsultation() {
+ when(beneficiaryFlowStatusRepo.getTCSpecialistWorkListNew(1, 5)).thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getTCSpecialistWorkListNewForTM(1, 5, 4));
+ assertEquals("[]", service.getTCSpecialistWorkListNewForTM(1, 5, 2));
+ }
+
+ @Test
+ void getTCSpecialistWorkListNewFutureScheduledForTM_onlyAppliesToTeleconsultation() {
+ when(beneficiaryFlowStatusRepo.getTCSpecialistWorkListNewFutureScheduled(1, 5))
+ .thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getTCSpecialistWorkListNewFutureScheduledForTM(1, 5, 4));
+ assertEquals("[]", service.getTCSpecialistWorkListNewFutureScheduledForTM(1, 5, 2));
+ }
+ }
+
+ @Nested
+ @DisplayName("referrals")
+ class Referrals {
+
+ private String referRequest(String extra) {
+ return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4,"
+ + "\"createdBy\":\"doctor\"" + extra + "}";
+ }
+
+ @Test
+ void saveBenReferDetails_createsOneRowPerAdditionalService() throws Exception {
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\","
+ + "\"referralReason\":\"Fever\",\"revisitDate\":\"2024-01-01T00:00:00.000\","
+ + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Tele-consultation\"},"
+ + "{\"serviceID\":2,\"serviceName\":\"Lab\"},{\"serviceID\":3}]");
+
+ assertEquals(1L, service.saveBenReferDetails(json(request)));
+ }
+
+ @Test
+ void saveBenReferDetails_storesTheReferralAsOneRowWhenNoServiceWasChosen() throws Exception {
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.saveBenReferDetails(json(referRequest(
+ ",\"referredToInstituteName\":\"PHC\""))));
+ }
+
+ @Test
+ void saveBenReferDetails_storesNothingWhenTheReferralIsEmpty() throws Exception {
+ when(benReferDetailsRepo.saveAll(any())).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenReferDetails(json(referRequest(""))));
+ }
+
+ @Test
+ void saveBenReferDetailsTMreferred_reusesTheExistingRowWhenTheInstituteWasUpdated() throws Exception {
+ when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), eq("U")))
+ .thenReturn(1);
+
+ assertEquals(1L, service.saveBenReferDetailsTMreferred(json(referRequest(
+ ",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\""))));
+ }
+
+ @Test
+ void saveBenReferDetailsTMreferred_createsOneRowPerAdditionalServiceWhenNoRowExisted() throws Exception {
+ when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), any())).thenReturn(0);
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\","
+ + "\"revisitDate\":\"2024-01-01T00:00:00.000\","
+ + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Lab\"},{}]");
+
+ assertEquals(1L, service.saveBenReferDetailsTMreferred(json(request)));
+ }
+
+ @Test
+ void saveBenReferDetailsTMreferred_storesTheReferralAsOneRowWhenNoServiceWasChosen() throws Exception {
+ when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), any())).thenReturn(0);
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.saveBenReferDetailsTMreferred(
+ json(referRequest(",\"referredToInstituteName\":\"PHC\""))));
+ }
+
+ @Test
+ void updateBenReferDetails_refreshesTheStoredRowsAndAddsTheNewServices() throws Exception {
+ ArrayList statuses = new ArrayList<>();
+ statuses.add(new Object[] { 5L, "P", "Lab" });
+ when(benReferDetailsRepo.getBenReferDetailsStatus(1L, 3L)).thenReturn(statuses);
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\","
+ + "\"referralReason\":\"Fever\",\"revisitDate\":\"2024-01-01T00:00:00.000\","
+ + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Lab\"},"
+ + "{\"serviceID\":2,\"serviceName\":\"Tele-consultation\"},{\"serviceID\":3,"
+ + "\"serviceName\":\"Radiology\"}]");
+
+ assertEquals(1L, service.updateBenReferDetails(json(request)));
+ verify(benReferDetailsRepo).updateReferredInstituteName(any(), any(), any(), any(), eq(5L), eq("U"));
+ }
+
+ @Test
+ void updateBenReferDetails_keepsARowThatWasNeverSyncedMarkedAsNew() throws Exception {
+ ArrayList statuses = new ArrayList<>();
+ statuses.add(new Object[] { 5L, "N", "Lab" });
+ when(benReferDetailsRepo.getBenReferDetailsStatus(any(), any())).thenReturn(statuses);
+ when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.updateBenReferDetails(
+ json(referRequest(",\"referredToInstituteName\":\"PHC\""))));
+ verify(benReferDetailsRepo).updateReferredInstituteName(any(), any(), any(), any(), eq(5L), eq("N"));
+ }
+
+ @Test
+ void updateBenReferDetails_leavesTheStoredRowsAloneWhenNothingWasEntered() throws Exception {
+ when(benReferDetailsRepo.getBenReferDetailsStatus(any(), any())).thenReturn(new ArrayList<>());
+ when(benReferDetailsRepo.saveAll(any())).thenReturn(new ArrayList<>());
+
+ assertEquals(1L, service.updateBenReferDetails(json(referRequest(""))));
+ }
+
+ @Test
+ void getReferralDetails_mapsTheStoredReferral() {
+ when(benReferDetailsRepo.getBenReferDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getReferralDetails(1L, 2L));
+ }
+ }
+
+ @Nested
+ @DisplayName("investigations and prescriptions")
+ class InvestigationsAndPrescriptions {
+
+ @Test
+ void getInvestigationDetails_mapsTheStoredOrders() {
+ when(labTestOrderDetailRepo.getLabTestOrderDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getInvestigationDetails(1L, 2L));
+ }
+
+ @Test
+ void getPrescribedDrugs_mapsTheStoredDrugs() {
+ when(prescribedDrugDetailRepo.getBenPrescribedDrugDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ assertEquals("[]", service.getPrescribedDrugs(1L, 2L));
+ }
+
+ @Test
+ void deletePrescribedMedicine_reportsWhetherTheRowWasRemoved() throws Exception {
+ JSONObject request = new JSONObject();
+ request.put("id", 5L);
+ when(prescribedDrugDetailRepo.deletePrescribedmedicine(5L)).thenReturn(1);
+ assertEquals("record deleted successfully", service.deletePrescribedMedicine(request));
+
+ when(prescribedDrugDetailRepo.deletePrescribedmedicine(5L)).thenReturn(0);
+ assertNull(service.deletePrescribedMedicine(request));
+ assertNull(service.deletePrescribedMedicine(new JSONObject()));
+ assertNull(service.deletePrescribedMedicine(null));
+ }
+ }
+
+ @Nested
+ @DisplayName("beneficiary flow after doctor data")
+ class BeneficiaryFlow {
+
+ private CommonUtilityClass utility(Boolean isSpecialist) {
+ CommonUtilityClass utility = new CommonUtilityClass();
+ utility.setBenFlowID(1L);
+ utility.setBeneficiaryID(2L);
+ utility.setBenVisitID(3L);
+ utility.setBeneficiaryRegID(4L);
+ utility.setVisitCode(5L);
+ utility.setCreatedBy("doctor");
+ utility.setIsSpecialist(isSpecialist);
+ return utility;
+ }
+
+ private TeleconsultationRequestOBJ teleconsultationRequest() {
+ TeleconsultationRequestOBJ request = new TeleconsultationRequestOBJ();
+ request.setUserID(7);
+ request.setAllocationDate(new Timestamp(System.currentTimeMillis()));
+ return request;
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataSave_recordsTheDoctorAndSendsTheBeneficiaryToTheLabAndPharmacy()
+ throws Exception {
+ Users doctor = new Users();
+ doctor.setUserID(11L);
+ when(userLoginRepo.getUserByUsername("doctor")).thenReturn(doctor);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), true, true,
+ teleconsultationRequest(), true));
+ verify(benVisitDetailRepo).updateDoctorID(11L, 5L);
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(eq(1L), eq(4L), eq(2L), eq(3L),
+ eq((short) 2), eq((short) 1), eq((short) 0), eq((short) 1), eq(7), any(), eq(true));
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataSave_closesTheDoctorStepWhenNothingWasPrescribed() throws Exception {
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), false, false, null, false));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(eq(1L), eq(4L), eq(2L), eq(3L),
+ eq((short) 9), eq((short) 0), eq((short) 0), eq((short) 0), eq(0), any(), eq(false));
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataSave_marksAnNcdScreeningTeleconsultationReferral() throws Exception {
+ ReflectionTestUtils.setField(service, "TMReferred", 1);
+ BeneficiaryFlowStatus stored = new BeneficiaryFlowStatus();
+ stored.setVisitCategory("NCD screening");
+ when(beneficiaryFlowStatusRepo.specialistFlagAndCategoryValue(5L)).thenReturn(stored);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), false, false, null, false));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), eq((short) 100), anyInt(), any(), any());
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataSave_leavesTheDoctorUnresolvedWhenTheUsernameIsUnknown() throws Exception {
+ CommonUtilityClass utility = utility(false);
+ utility.setCreatedBy(" ");
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility, false, false, null, false));
+ verify(benVisitDetailRepo, never()).updateDoctorID(anyLong(), anyLong());
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataSave_leavesTheDoctorUnresolvedWhenTheVisitHasNoCode() throws Exception {
+ CommonUtilityClass utility = utility(false);
+ utility.setVisitCode(null);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility, false, false, null, false));
+ verify(benVisitDetailRepo, never()).updateDoctorID(anyLong(), anyLong());
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataUpdate_routesASpecialistUpdateThroughTheSpecialistFlow() throws Exception {
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(),
+ any(), anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(true), true, true, null, true));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateTCSpecialist(eq(1L), eq(4L), eq(2L),
+ eq(3L), eq((short) 0), eq((short) 1), eq((short) 0), eq((short) 2), eq(0), any(), eq(true));
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataUpdate_closesTheSpecialistStepWhenNothingWasPrescribed() throws Exception {
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(),
+ any(), anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(true), false, false, null, false));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(),
+ any(), anyShort(), eq((short) 0), anyShort(), eq((short) 9), anyInt(), any(), any());
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataUpdate_usesTheWalkInFlowForAnOrdinaryDoctorUpdate() throws Exception {
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateWDF(any(), any(), any(), any(),
+ anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(false), true, true,
+ teleconsultationRequest(), true));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateWDF(eq(1L), eq(4L), eq(2L), eq(3L),
+ eq((short) 2), eq((short) 1), eq((short) 0), eq(7), any(), eq(true));
+ }
+
+ @Test
+ void updateBenFlowtableAfterDocDataUpdate_usesTheReferralFlowForAnNcdScreeningReferral() throws Exception {
+ ReflectionTestUtils.setField(service, "TMReferred", 1);
+ BeneficiaryFlowStatus stored = new BeneficiaryFlowStatus();
+ stored.setVisitCategory("NCD screening");
+ when(beneficiaryFlowStatusRepo.specialistFlagAndCategoryValue(5L)).thenReturn(stored);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdate(any(), any(), any(), any(),
+ anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(false), false, false, null, false));
+ verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdate(any(), any(), any(), any(),
+ eq((short) 9), eq((short) 0), anyShort(), eq((short) 100), anyInt(), any(), any());
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java
new file mode 100644
index 00000000..dfe792e1
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java
@@ -0,0 +1,2479 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.common.transaction;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.Date;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.iemr.mmu.data.anc.BenAllergyHistory;
+import com.iemr.mmu.data.anc.BenFamilyHistory;
+import com.iemr.mmu.data.anc.BenMedHistory;
+import com.iemr.mmu.data.anc.BenMedicationHistory;
+import com.iemr.mmu.data.anc.BenMenstrualDetails;
+import com.iemr.mmu.data.anc.BenPersonalHabit;
+import com.iemr.mmu.data.anc.BencomrbidityCondDetails;
+import com.iemr.mmu.data.anc.ChildOptionalVaccineDetail;
+import com.iemr.mmu.data.anc.ChildVaccineDetail1;
+import com.iemr.mmu.data.anc.FemaleObstetricHistory;
+import com.iemr.mmu.data.anc.PhyGeneralExamination;
+import com.iemr.mmu.data.anc.PhyHeadToToeExamination;
+import com.iemr.mmu.data.anc.SysCardiovascularExamination;
+import com.iemr.mmu.data.anc.SysCentralNervousExamination;
+import com.iemr.mmu.data.anc.SysGastrointestinalExamination;
+import com.iemr.mmu.data.anc.SysGenitourinarySystemExamination;
+import com.iemr.mmu.data.anc.SysMusculoskeletalSystemExamination;
+import com.iemr.mmu.data.anc.SysRespiratoryExamination;
+import com.iemr.mmu.data.anc.WrapperChildOptionalVaccineDetail;
+import com.iemr.mmu.data.anc.WrapperComorbidCondDetails;
+import com.iemr.mmu.data.anc.WrapperFemaleObstetricHistory;
+import com.iemr.mmu.data.anc.WrapperImmunizationHistory;
+import com.iemr.mmu.data.anc.WrapperMedicationHistory;
+import com.iemr.mmu.data.login.Users;
+import com.iemr.mmu.data.ncdScreening.IDRSData;
+import com.iemr.mmu.data.ncdScreening.PhysicalActivityType;
+import com.iemr.mmu.data.nurse.BenAnthropometryDetail;
+import com.iemr.mmu.data.nurse.BenPhysicalVitalDetail;
+import com.iemr.mmu.data.nurse.BeneficiaryVisitDetail;
+import com.iemr.mmu.data.quickConsultation.BenChiefComplaint;
+import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo;
+import com.iemr.mmu.repo.bmiCalculation.BMICalculationRepo;
+import com.iemr.mmu.repo.login.UserLoginRepo;
+import com.iemr.mmu.repo.nurse.BenAnthropometryRepo;
+import com.iemr.mmu.repo.nurse.BenCancerVitalDetailRepo;
+import com.iemr.mmu.repo.nurse.BenPhysicalVitalRepo;
+import com.iemr.mmu.repo.nurse.BenVisitDetailRepo;
+import com.iemr.mmu.repo.nurse.anc.BenAdherenceRepo;
+import com.iemr.mmu.repo.nurse.anc.BenAllergyHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.BenChildDevelopmentHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.BenFamilyHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.BenMedHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.BenMedicationHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.BenMenstrualDetailsRepo;
+import com.iemr.mmu.repo.nurse.anc.BenPersonalHabitRepo;
+import com.iemr.mmu.repo.nurse.anc.BencomrbidityCondRepo;
+import com.iemr.mmu.repo.nurse.anc.ChildFeedingDetailsRepo;
+import com.iemr.mmu.repo.nurse.anc.ChildOptionalVaccineDetailRepo;
+import com.iemr.mmu.repo.nurse.anc.ChildVaccineDetail1Repo;
+import com.iemr.mmu.repo.nurse.anc.FemaleObstetricHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.PerinatalHistoryRepo;
+import com.iemr.mmu.repo.nurse.anc.PhyGeneralExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.PhyHeadToToeExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysCardiovascularExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysCentralNervousExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysGastrointestinalExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysGenitourinarySystemExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysMusculoskeletalSystemExaminationRepo;
+import com.iemr.mmu.repo.nurse.anc.SysRespiratoryExaminationRepo;
+import com.iemr.mmu.repo.nurse.ncdscreening.IDRSDataRepo;
+import com.iemr.mmu.repo.nurse.ncdscreening.PhysicalActivityTypeRepo;
+import com.iemr.mmu.repo.quickConsultation.BenChiefComplaintRepo;
+import com.iemr.mmu.repo.quickConsultation.LabTestOrderDetailRepo;
+import com.iemr.mmu.repo.quickConsultation.PrescribedDrugDetailRepo;
+import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo;
+import com.iemr.mmu.repo.registrar.RegistrarRepoBenData;
+import com.iemr.mmu.repo.registrar.ReistrarRepoBenSearch;
+import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption;
+import com.iemr.mmu.utils.exception.IEMRException;
+
+class CommonNurseServiceImplTest {
+
+ @Mock
+ private BenVisitDetailRepo benVisitDetailRepo;
+ @Mock
+ private UserLoginRepo userLoginRepo;
+ @Mock
+ private BenChiefComplaintRepo benChiefComplaintRepo;
+ @Mock
+ private BenMedHistoryRepo benMedHistoryRepo;
+ @Mock
+ private BencomrbidityCondRepo bencomrbidityCondRepo;
+ @Mock
+ private BenMedicationHistoryRepo benMedicationHistoryRepo;
+ @Mock
+ private FemaleObstetricHistoryRepo femaleObstetricHistoryRepo;
+ @Mock
+ private BenMenstrualDetailsRepo benMenstrualDetailsRepo;
+ @Mock
+ private BenFamilyHistoryRepo benFamilyHistoryRepo;
+ @Mock
+ private BenPersonalHabitRepo benPersonalHabitRepo;
+ @Mock
+ private BenAllergyHistoryRepo benAllergyHistoryRepo;
+ @Mock
+ private ChildOptionalVaccineDetailRepo childOptionalVaccineDetailRepo;
+ @Mock
+ private ChildVaccineDetail1Repo childVaccineDetail1Repo;
+ @Mock
+ private BenAnthropometryRepo benAnthropometryRepo;
+ @Mock
+ private BenPhysicalVitalRepo benPhysicalVitalRepo;
+ @Mock
+ private PhyGeneralExaminationRepo phyGeneralExaminationRepo;
+ @Mock
+ private PhyHeadToToeExaminationRepo phyHeadToToeExaminationRepo;
+ @Mock
+ private SysGastrointestinalExaminationRepo sysGastrointestinalExaminationRepo;
+ @Mock
+ private SysCardiovascularExaminationRepo sysCardiovascularExaminationRepo;
+ @Mock
+ private SysRespiratoryExaminationRepo sysRespiratoryExaminationRepo;
+ @Mock
+ private SysCentralNervousExaminationRepo sysCentralNervousExaminationRepo;
+ @Mock
+ private SysMusculoskeletalSystemExaminationRepo sysMusculoskeletalSystemExaminationRepo;
+ @Mock
+ private SysGenitourinarySystemExaminationRepo sysGenitourinarySystemExaminationRepo;
+ @Mock
+ private RegistrarRepoBenData registrarRepoBenData;
+ @Mock
+ private PrescriptionDetailRepo prescriptionDetailRepo;
+ @Mock
+ private LabTestOrderDetailRepo labTestOrderDetailRepo;
+ @Mock
+ private PrescribedDrugDetailRepo prescribedDrugDetailRepo;
+ @Mock
+ private ReistrarRepoBenSearch reistrarRepoBenSearch;
+ @Mock
+ private BenAdherenceRepo benAdherenceRepo;
+ @Mock
+ private BenChildDevelopmentHistoryRepo benChildDevelopmentHistoryRepo;
+ @Mock
+ private ChildFeedingDetailsRepo childFeedingDetailsRepo;
+ @Mock
+ private PerinatalHistoryRepo perinatalHistoryRepo;
+ @Mock
+ private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo;
+ @Mock
+ private BenCancerVitalDetailRepo benCancerVitalDetailRepo;
+ @Mock
+ private CommonDoctorServiceImpl commonDoctorServiceImpl;
+ @Mock
+ private PhysicalActivityTypeRepo physicalActivityTypeRepo;
+ @Mock
+ private AESEncryptionDecryption aESEncryptionDecryption;
+ @Mock
+ private IDRSDataRepo iDRSDataRepo;
+ @Mock
+ private BMICalculationRepo bmiCalculationRepo;
+
+ @InjectMocks
+ private CommonNurseServiceImpl service;
+
+ private AutoCloseable mocks;
+
+ @BeforeEach
+ void setUp() {
+ mocks = MockitoAnnotations.openMocks(this);
+ }
+
+ @Nested
+ @DisplayName("beneficiary visit details")
+ class VisitDetails {
+
+ @Test
+ void saveBeneficiaryVisitDetails_incrementsVisitCountAndResolvesNurse() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ detail.setBeneficiaryRegID(11L);
+ detail.setCreatedBy("nurse1");
+ detail.setFileIDs(new String[] { "a", "b" });
+
+ Users user = new Users();
+ user.setUserID(77L);
+ when(userLoginRepo.getUserByUsername("nurse1")).thenReturn(user);
+ when(benVisitDetailRepo.getVisitCountForBeneficiary(11L)).thenReturn((short) 3);
+
+ BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail();
+ saved.setBenVisitID(99L);
+ when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved);
+
+ assertEquals(99L, service.saveBeneficiaryVisitDetails(detail));
+ assertEquals((short) 4, detail.getVisitNo());
+ assertEquals("a,b,", detail.getReportFilePath());
+ assertEquals(77L, detail.getNurseID());
+ verify(benVisitDetailRepo).updateVanSerialNo(99L);
+ }
+
+ @Test
+ void saveBeneficiaryVisitDetails_startsAtVisitOneWhenBeneficiaryHasNoHistory() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ detail.setBeneficiaryRegID(11L);
+ when(benVisitDetailRepo.getVisitCountForBeneficiary(11L)).thenReturn(null);
+
+ BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail();
+ saved.setBenVisitID(5L);
+ when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved);
+
+ assertEquals(5L, service.saveBeneficiaryVisitDetails(detail));
+ assertEquals((short) 1, detail.getVisitNo());
+ assertEquals("", detail.getReportFilePath());
+ assertNull(detail.getNurseID());
+ }
+
+ @Test
+ void saveBeneficiaryVisitDetails_leavesNurseUnresolvedWhenUsernameIsUnknown() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ detail.setCreatedBy(" ");
+
+ BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail();
+ saved.setBenVisitID(1L);
+ when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved);
+
+ service.saveBeneficiaryVisitDetails(detail);
+ assertNull(detail.getNurseID());
+ verify(userLoginRepo, never()).getUserByUsername(anyString());
+ }
+
+ @Test
+ void saveBeneficiaryVisitDetails_leavesNurseUnresolvedWhenLookupFindsNoUser() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ detail.setCreatedBy("ghost");
+ when(userLoginRepo.getUserByUsername("ghost")).thenReturn(null);
+
+ BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail();
+ saved.setBenVisitID(1L);
+ when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved);
+
+ service.saveBeneficiaryVisitDetails(detail);
+ assertNull(detail.getNurseID());
+ }
+
+ @Test
+ void getBenVisitCount_returnsNextVisitNumber() {
+ when(benVisitDetailRepo.getVisitCountForBeneficiary(1L)).thenReturn((short) 2);
+ assertEquals((short) 3, service.getBenVisitCount(1L));
+
+ when(benVisitDetailRepo.getVisitCountForBeneficiary(2L)).thenReturn(null);
+ assertEquals((short) 1, service.getBenVisitCount(2L));
+ }
+
+ @Test
+ void updateBeneficiaryStatus_delegatesToRegistrarRepo() {
+ when(registrarRepoBenData.updateBenFlowStatus('N', 4L)).thenReturn(1);
+ assertEquals(1, service.updateBeneficiaryStatus('N', 4L));
+ }
+
+ @Test
+ void getMaxCurrentdate_returnsZeroWhenNoPreviousVisitExists() throws Exception {
+ when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(null);
+ assertEquals(0, service.getMaxCurrentdate(1L, "reason", "category"));
+ }
+
+ @Test
+ void getMaxCurrentdate_returnsPositiveWhileThePreviousVisitIsStillWithinTenMinutes() throws Exception {
+ String recent = new java.sql.Timestamp(System.currentTimeMillis()).toString();
+ when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(recent);
+ assertTrue(service.getMaxCurrentdate(1L, "reason", "category") > 0);
+ }
+
+ @Test
+ void getMaxCurrentdate_returnsNegativeOnceThePreviousVisitIsOlderThanTenMinutes() throws Exception {
+ String old = new java.sql.Timestamp(System.currentTimeMillis() - 3600_000L).toString();
+ when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(old);
+ assertTrue(service.getMaxCurrentdate(1L, "reason", "category") < 0);
+ }
+
+ @Test
+ void getMaxCurrentdate_wrapsAnUnparseableStoredDate() {
+ when(benVisitDetailRepo.getMaxCreatedDate(1L, "r", "c")).thenReturn("not-a-date.0");
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> service.getMaxCurrentdate(1L, "r", "c"));
+ assertTrue(thrown.getMessage().contains("Error while parseing created date"));
+ }
+
+ @Test
+ void generateVisitCode_padsVanAndVisitIdsIntoAFourteenDigitCode() {
+ when(benVisitDetailRepo.updateVisitCode(anyLong(), anyLong())).thenReturn(1);
+ assertEquals(Long.valueOf("10000100000123"), service.generateVisitCode(123L, 1, 1));
+ }
+
+ @Test
+ void generateVisitCode_returnsZeroWhenTheCodeCouldNotBeStored() {
+ when(benVisitDetailRepo.updateVisitCode(anyLong(), anyLong())).thenReturn(0);
+ assertEquals(0L, service.generateVisitCode(123L, 1, 1));
+ }
+
+ @Test
+ void updateVisitCodeInVisitDetailsTable_delegatesToRepo() {
+ when(benVisitDetailRepo.updateVisitCode(5L, 6L)).thenReturn(1);
+ assertEquals(1, service.updateVisitCodeInVisitDetailsTable(5L, 6L));
+ }
+
+ @Test
+ void updateBeneficiaryVisitDetails_returnsRepoResult() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ when(benVisitDetailRepo.updateBeneficiaryVisitDetail(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any())).thenReturn(1);
+ assertEquals(1, service.updateBeneficiaryVisitDetails(detail));
+ }
+
+ @Test
+ void updateBeneficiaryVisitDetails_returnsZeroWhenTheRepoFails() {
+ BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail();
+ when(benVisitDetailRepo.updateBeneficiaryVisitDetail(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any())).thenThrow(new RuntimeException("db down"));
+ assertEquals(0, service.updateBeneficiaryVisitDetails(detail));
+ }
+
+ @Test
+ void getCSVisitDetails_returnsNullWhenTheVisitDoesNotExist() throws Exception {
+ when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(null);
+ assertNull(service.getCSVisitDetails(1L, 2L));
+ }
+
+ @Test
+ void getCSVisitDetails_decryptsEachAttachedReportPath() throws Exception {
+ BeneficiaryVisitDetail stored = new BeneficiaryVisitDetail();
+ stored.setBenVisitID(1L);
+ stored.setReportFilePath("enc1,,enc2");
+ when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(stored);
+ when(aESEncryptionDecryption.decrypt("enc1")).thenReturn("/tmp/reports/first.pdf");
+ when(aESEncryptionDecryption.decrypt("enc2")).thenReturn("/tmp/reports/second.pdf");
+
+ BeneficiaryVisitDetail result = service.getCSVisitDetails(1L, 2L);
+
+ assertNotNull(result);
+ assertEquals(2, result.getFiles().size());
+ assertEquals("first.pdf", result.getFiles().get(0).get("fileName"));
+ assertEquals("enc2", result.getFiles().get(1).get("filePath"));
+ }
+
+ @Test
+ void getCSVisitDetails_returnsAnEmptyFileListWhenNoReportsAreAttached() throws Exception {
+ BeneficiaryVisitDetail stored = new BeneficiaryVisitDetail();
+ stored.setBenVisitID(1L);
+ stored.setReportFilePath(" ");
+ when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(stored);
+
+ assertTrue(service.getCSVisitDetails(1L, 2L).getFiles().isEmpty());
+ }
+ }
+
+ @Nested
+ @DisplayName("history and examination saves")
+ class HistorySaves {
+
+ @Test
+ void saveBenChiefComplaints_skipsEntriesWithoutAComplaintId() {
+ BenChiefComplaint withId = new BenChiefComplaint();
+ withId.setChiefComplaintID(1);
+ withId.setBenChiefComplaintID(10L);
+ BenChiefComplaint withoutId = new BenChiefComplaint();
+
+ when(benChiefComplaintRepo.saveAll(any())).thenReturn(Collections.singletonList(withId));
+
+ assertEquals(1, service.saveBenChiefComplaints(Arrays.asList(withId, withoutId)));
+ verify(benChiefComplaintRepo).updateVanSerialNo(10L);
+ }
+
+ @Test
+ void saveBenChiefComplaints_succeedsWhenThereIsNothingToSave() {
+ assertEquals(1, service.saveBenChiefComplaints(new ArrayList<>()));
+ verify(benChiefComplaintRepo, never()).saveAll(any());
+ }
+
+ @Test
+ void saveBenChiefComplaints_reportsFailureWhenNotEveryComplaintWasSaved() {
+ BenChiefComplaint withId = new BenChiefComplaint();
+ withId.setChiefComplaintID(1);
+ when(benChiefComplaintRepo.saveAll(any())).thenReturn(new ArrayList<>());
+ assertEquals(0, service.saveBenChiefComplaints(Collections.singletonList(withId)));
+ }
+
+ @Test
+ void saveBenPastHistory_savesEveryPastHistoryEntry() {
+ BenMedHistory history = mock(BenMedHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory()));
+ when(history.getBenPastHistory()).thenReturn(entries);
+ when(benMedHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveBenPastHistory(history));
+ }
+
+ @Test
+ void saveBenPastHistory_succeedsWhenThereIsNoPastHistory() {
+ BenMedHistory history = mock(BenMedHistory.class);
+ when(history.getBenPastHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenPastHistory(history));
+ }
+
+ @Test
+ void saveBenPastHistory_reportsFailureWhenNotEveryEntryWasSaved() {
+ BenMedHistory history = mock(BenMedHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory()));
+ when(history.getBenPastHistory()).thenReturn(entries);
+ when(benMedHistoryRepo.saveAll(entries)).thenReturn(new ArrayList());
+ assertNull(service.saveBenPastHistory(history));
+ }
+
+ @Test
+ void saveBenComorbidConditions_returnsTheIdOfTheFirstStoredCondition() {
+ WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class);
+ BencomrbidityCondDetails stored = new BencomrbidityCondDetails();
+ stored.setID(42L);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(stored));
+ when(wrapper.getComrbidityConds()).thenReturn(entries);
+ when(bencomrbidityCondRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(42L, service.saveBenComorbidConditions(wrapper));
+ }
+
+ @Test
+ void saveBenComorbidConditions_succeedsWhenThereAreNoConditions() {
+ WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class);
+ when(wrapper.getComrbidityConds()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenComorbidConditions(wrapper));
+ }
+
+ @Test
+ void saveBenMedicationHistory_returnsTheIdOfTheFirstStoredEntry() {
+ WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class);
+ BenMedicationHistory stored = new BenMedicationHistory();
+ stored.setID(7L);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(stored));
+ when(wrapper.getBenMedicationHistoryDetails()).thenReturn(entries);
+ when(benMedicationHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(7L, service.saveBenMedicationHistory(wrapper));
+ }
+
+ @Test
+ void saveBenMedicationHistory_succeedsWhenThereIsNoMedicationHistory() {
+ WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class);
+ when(wrapper.getBenMedicationHistoryDetails()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenMedicationHistory(wrapper));
+ }
+
+ @Test
+ void saveFemaleObstetricHistory_flattensEveryComplicationListOntoTheStoredRow() {
+ FemaleObstetricHistory entry = new FemaleObstetricHistory();
+ entry.setPregComplicationList(complications("pregComplicationID", "pregComplicationType"));
+ entry.setDeliveryComplicationList(complications("deliveryComplicationID", "deliveryComplicationType"));
+ entry.setPostpartumComplicationList(
+ complications("postpartumComplicationID", "postpartumComplicationType"));
+
+ ArrayList> postAbortion = new ArrayList<>();
+ postAbortion.add(complication(1d, "first"));
+ postAbortion.add(complication(2d, "second"));
+ entry.setPostAbortionComplication(postAbortion);
+ entry.setAbortionType(complication(3d, "abortion"));
+
+ Map facility = new HashMap<>();
+ facility.put("serviceFacilityID", 4d);
+ facility.put("facilityName", "PHC");
+ entry.setTypeofFacility(facility);
+
+ WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(entry));
+ when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries);
+ when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveFemaleObstetricHistory(wrapper));
+ assertEquals("11,12", entry.getPregComplicationID());
+ assertEquals("name11,name12", entry.getPregComplicationType());
+ assertEquals("11,12", entry.getDeliveryComplicationID());
+ assertEquals("11,12", entry.getPostpartumComplicationID());
+ assertEquals("1,2", entry.getPostAbortionComplication_db());
+ assertEquals("first,second", entry.getPostAbortionComplicationsValues());
+ assertEquals(3, entry.getAbortionTypeID());
+ assertEquals("abortion", entry.getTypeOfAbortionValue());
+ assertEquals(4, entry.getTypeofFacilityID());
+ assertEquals("PHC", entry.getServiceFacilityValue());
+ }
+
+ @Test
+ void saveFemaleObstetricHistory_leavesComplicationFieldsEmptyWhenNoneWereReported() {
+ FemaleObstetricHistory entry = new FemaleObstetricHistory();
+ WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(entry));
+ when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries);
+ when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveFemaleObstetricHistory(wrapper));
+ assertEquals("", entry.getPregComplicationID());
+ assertNull(entry.getPostAbortionComplication_db());
+ }
+
+ @Test
+ void saveFemaleObstetricHistory_succeedsWhenThereIsNoObstetricHistory() {
+ WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class);
+ when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveFemaleObstetricHistory(wrapper));
+ }
+
+ private ArrayList> complications(String idKey, String nameKey) {
+ ArrayList> list = new ArrayList<>();
+ for (int i = 11; i <= 12; i++) {
+ Map item = new HashMap<>();
+ item.put(idKey, i);
+ item.put(nameKey, "name" + i);
+ list.add(item);
+ }
+ return list;
+ }
+
+ private Map complication(Double id, String value) {
+ Map map = new HashMap<>();
+ map.put("complicationID", id);
+ map.put("complicationValue", value);
+ return map;
+ }
+
+ @Test
+ void saveBenMenstrualHistory_flattensTheReportedProblemsBeforeSaving() {
+ BenMenstrualDetails details = new BenMenstrualDetails();
+ ArrayList> problems = new ArrayList<>();
+ for (int i = 1; i <= 2; i++) {
+ Map problem = new HashMap<>();
+ problem.put("menstrualProblemID", i);
+ problem.put("problemName", "problem" + i);
+ problems.add(problem);
+ }
+ details.setMenstrualProblemList(problems);
+
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ stored.setBenMenstrualID(9);
+ when(benMenstrualDetailsRepo.save(details)).thenReturn(stored);
+
+ assertEquals(9, service.saveBenMenstrualHistory(details));
+ assertEquals("1,2", details.getMenstrualProblemID());
+ assertEquals("problem1,problem2", details.getProblemName());
+ }
+
+ @Test
+ void saveBenMenstrualHistory_returnsNullWhenTheRowWasNotPersisted() {
+ BenMenstrualDetails details = new BenMenstrualDetails();
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ stored.setBenMenstrualID(0);
+ when(benMenstrualDetailsRepo.save(details)).thenReturn(stored);
+ assertNull(service.saveBenMenstrualHistory(details));
+ }
+
+ @Test
+ void saveBenFamilyHistory_savesEveryReportedFamilyDisease() {
+ BenFamilyHistory input = mock(BenFamilyHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory()));
+ when(input.getBenFamilyHistory()).thenReturn(entries);
+ when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveBenFamilyHistory(input));
+ }
+
+ @Test
+ void saveBenFamilyHistory_succeedsWhenThereIsNoFamilyHistory() {
+ BenFamilyHistory input = mock(BenFamilyHistory.class);
+ when(input.getBenFamilyHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenFamilyHistory(input));
+ }
+
+ @Test
+ void saveBenFamilyHistoryNCDScreening_savesTheScreeningVariantOfTheList() {
+ BenFamilyHistory input = mock(BenFamilyHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory()));
+ when(input.getBenFamilyHist()).thenReturn(entries);
+ when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveBenFamilyHistoryNCDScreening(input));
+ }
+
+ @Test
+ void saveBenFamilyHistoryNCDScreening_succeedsWhenNothingWasScreened() {
+ BenFamilyHistory input = mock(BenFamilyHistory.class);
+ when(input.getBenFamilyHist()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveBenFamilyHistoryNCDScreening(input));
+ }
+
+ @Test
+ void savePersonalHistory_savesEveryReportedHabit() {
+ BenPersonalHabit input = mock(BenPersonalHabit.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenPersonalHabit()));
+ when(input.getPersonalHistory()).thenReturn(entries);
+ when(benPersonalHabitRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.savePersonalHistory(input));
+ }
+
+ @Test
+ void savePersonalHistory_succeedsWhenNoHabitsWereReported() {
+ BenPersonalHabit input = mock(BenPersonalHabit.class);
+ when(input.getPersonalHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.savePersonalHistory(input));
+ }
+
+ @Test
+ void saveAllergyHistory_savesEveryReportedAllergy() {
+ BenAllergyHistory input = mock(BenAllergyHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenAllergyHistory()));
+ when(input.getBenAllergicHistory()).thenReturn(entries);
+ when(benAllergyHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveAllergyHistory(input));
+ }
+
+ @Test
+ void saveAllergyHistory_succeedsWhenNoAllergiesWereReported() {
+ BenAllergyHistory input = mock(BenAllergyHistory.class);
+ when(input.getBenAllergicHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveAllergyHistory(input));
+ }
+
+ @Test
+ void saveChildOptionalVaccineDetail_savesEveryOptionalVaccine() {
+ WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class);
+ ArrayList entries = new ArrayList<>(
+ Collections.singletonList(new ChildOptionalVaccineDetail()));
+ when(wrapper.getChildOptionalVaccineDetails()).thenReturn(entries);
+ when(childOptionalVaccineDetailRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1L, service.saveChildOptionalVaccineDetail(wrapper));
+ }
+
+ @Test
+ void saveChildOptionalVaccineDetail_succeedsWhenNoOptionalVaccineWasGiven() {
+ WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class);
+ when(wrapper.getChildOptionalVaccineDetails()).thenReturn(new ArrayList<>());
+ assertEquals(1L, service.saveChildOptionalVaccineDetail(wrapper));
+ }
+
+ @Test
+ void saveImmunizationHistory_returnsTheIdOfTheFirstStoredVaccine() {
+ WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class);
+ ChildVaccineDetail1 stored = new ChildVaccineDetail1();
+ stored.setID(3L);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(stored));
+ when(wrapper.getBenChildVaccineDetails()).thenReturn(entries);
+ when(childVaccineDetail1Repo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(3L, service.saveImmunizationHistory(wrapper));
+ }
+
+ @Test
+ void saveImmunizationHistory_returnsNullWhenNoVaccineWasStored() {
+ WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class);
+ ArrayList entries = new ArrayList<>();
+ when(wrapper.getBenChildVaccineDetails()).thenReturn(entries);
+ when(childVaccineDetail1Repo.saveAll(entries)).thenReturn(entries);
+
+ assertNull(service.saveImmunizationHistory(wrapper));
+ }
+ }
+
+ @Nested
+ @DisplayName("vitals and anthropometry")
+ class Vitals {
+
+ @Test
+ void saveBeneficiaryPhysicalAnthropometryDetails_returnsTheStoredId() {
+ BenAnthropometryDetail detail = new BenAnthropometryDetail();
+ BenAnthropometryDetail stored = new BenAnthropometryDetail();
+ stored.setID(5L);
+ when(benAnthropometryRepo.save(detail)).thenReturn(stored);
+ assertEquals(5L, service.saveBeneficiaryPhysicalAnthropometryDetails(detail));
+ }
+
+ @Test
+ void saveBeneficiaryPhysicalVitalDetails_averagesEveryBloodPressureReadingTaken() {
+ BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail();
+ detail.setSystolicBP_1stReading((short) 120);
+ detail.setDiastolicBP_1stReading((short) 80);
+ detail.setSystolicBP_2ndReading((short) 130);
+ detail.setDiastolicBP_2ndReading((short) 90);
+ detail.setSystolicBP_3rdReading((short) 140);
+ detail.setDiastolicBP_3rdReading((short) 100);
+
+ BenPhysicalVitalDetail stored = new BenPhysicalVitalDetail();
+ stored.setID(8L);
+ when(benPhysicalVitalRepo.save(detail)).thenReturn(stored);
+
+ assertEquals(8L, service.saveBeneficiaryPhysicalVitalDetails(detail));
+ assertEquals((short) 130, detail.getAverageSystolicBP());
+ assertEquals((short) 90, detail.getAverageDiastolicBP());
+ }
+
+ @Test
+ void saveBeneficiaryPhysicalVitalDetails_leavesTheAverageUnsetWhenNoReadingWasTaken() {
+ BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail();
+ when(benPhysicalVitalRepo.save(detail)).thenReturn(null);
+
+ assertNull(service.saveBeneficiaryPhysicalVitalDetails(detail));
+ assertNull(detail.getAverageSystolicBP());
+ }
+
+ @Test
+ void getBeneficiaryPhysicalAnthropometryDetails_serialisesTheStoredRow() {
+ BenAnthropometryDetail stored = new BenAnthropometryDetail();
+ stored.setID(1L);
+ when(benAnthropometryRepo.getBenAnthropometryDetail(1L, 2L)).thenReturn(stored);
+ assertTrue(service.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L).contains("\"ID\":1"));
+ }
+
+ @Test
+ void getBeneficiaryPhysicalVitalDetails_serialisesTheStoredRow() {
+ BenPhysicalVitalDetail stored = new BenPhysicalVitalDetail();
+ stored.setID(2L);
+ when(benPhysicalVitalRepo.getBenPhysicalVitalDetail(1L, 2L)).thenReturn(stored);
+ assertTrue(service.getBeneficiaryPhysicalVitalDetails(1L, 2L).contains("\"ID\":2"));
+ }
+
+ @Test
+ void updateANCAnthropometryDetails_marksAnAlreadySyncedRowAsUpdated() {
+ BenAnthropometryDetail detail = new BenAnthropometryDetail();
+ detail.setBeneficiaryRegID(1L);
+ detail.setVisitCode(2L);
+ when(benAnthropometryRepo.getBenAnthropometryStatus(1L, 2L)).thenReturn("P");
+ when(benAnthropometryRepo.updateANCCareDetails(any(), any(), any(), any(), any(), any(), any(), any(),
+ any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updateANCAnthropometryDetails(detail));
+ }
+
+ @Test
+ void updateANCAnthropometryDetails_keepsARowThatWasNeverSyncedAsNew() {
+ BenAnthropometryDetail detail = new BenAnthropometryDetail();
+ detail.setBeneficiaryRegID(1L);
+ detail.setVisitCode(2L);
+ when(benAnthropometryRepo.getBenAnthropometryStatus(1L, 2L)).thenReturn("N");
+ when(benAnthropometryRepo.updateANCCareDetails(any(), any(), any(), any(), any(), any(), any(), any(),
+ any(), org.mockito.ArgumentMatchers.eq("N"), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updateANCAnthropometryDetails(detail));
+ }
+
+ @Test
+ void updateANCAnthropometryDetails_doesNothingWithoutARow() {
+ assertEquals(0, service.updateANCAnthropometryDetails(null));
+ }
+
+ @Test
+ void updateANCPhysicalVitalDetails_copiesTheFirstReadingIntoTheAverage() {
+ BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail();
+ detail.setBeneficiaryRegID(1L);
+ detail.setVisitCode(2L);
+ detail.setSystolicBP_1stReading((short) 118);
+ detail.setDiastolicBP_1stReading((short) 76);
+ when(benPhysicalVitalRepo.getBenPhysicalVitalStatus(1L, 2L)).thenReturn("P");
+ when(benPhysicalVitalRepo.updatePhysicalVitalDetails(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ anyString(), any(), any(), any(), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updateANCPhysicalVitalDetails(detail));
+ assertEquals((short) 118, detail.getAverageSystolicBP());
+ assertEquals((short) 76, detail.getAverageDiastolicBP());
+ }
+
+ @Test
+ void updateANCPhysicalVitalDetails_doesNothingWithoutARow() {
+ assertEquals(0, service.updateANCPhysicalVitalDetails(null));
+ }
+
+ @Test
+ void saveIDRS_returnsTheStoredId() {
+ IDRSData data = new IDRSData();
+ IDRSData stored = new IDRSData();
+ stored.setId(4L);
+ when(iDRSDataRepo.save(data)).thenReturn(stored);
+ assertEquals(4L, service.saveIDRS(data));
+ }
+
+ @Test
+ void saveIDRS_returnsNullWhenNothingWasStored() {
+ IDRSData data = new IDRSData();
+ when(iDRSDataRepo.save(data)).thenReturn(null);
+ assertNull(service.saveIDRS(data));
+ }
+
+ @Test
+ void savePhysicalActivity_returnsTheStoredId() {
+ PhysicalActivityType activity = new PhysicalActivityType();
+ PhysicalActivityType stored = new PhysicalActivityType();
+ stored.setpAID(6L);
+ when(physicalActivityTypeRepo.save(activity)).thenReturn(stored);
+ assertEquals(6L, service.savePhysicalActivity(activity));
+ }
+
+ @Test
+ void savePhysicalActivity_returnsNullWhenNothingWasStored() {
+ PhysicalActivityType activity = new PhysicalActivityType();
+ when(physicalActivityTypeRepo.save(activity)).thenReturn(null);
+ assertNull(service.savePhysicalActivity(activity));
+ }
+ }
+
+ @Nested
+ @DisplayName("examination saves")
+ class ExaminationSaves {
+
+ @Test
+ void savePhyGeneralExamination_joinsTheReportedDangerSigns() {
+ PhyGeneralExamination examination = new PhyGeneralExamination();
+ examination.setTypeOfDangerSigns(new ArrayList<>(Arrays.asList("fever", "bleeding")));
+ PhyGeneralExamination stored = new PhyGeneralExamination();
+ stored.setID(1L);
+ when(phyGeneralExaminationRepo.save(examination)).thenReturn(stored);
+
+ assertEquals(1L, service.savePhyGeneralExamination(examination));
+ assertEquals("fever,bleeding,", examination.getTypeOfDangerSign());
+ }
+
+ @Test
+ void savePhyGeneralExamination_leavesDangerSignsUnsetWhenNoneWereReported() {
+ PhyGeneralExamination examination = new PhyGeneralExamination();
+ when(phyGeneralExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.savePhyGeneralExamination(examination));
+ assertNull(examination.getTypeOfDangerSign());
+ }
+
+ @Test
+ void savePhyHeadToToeExamination_returnsTheStoredId() {
+ PhyHeadToToeExamination examination = new PhyHeadToToeExamination();
+ PhyHeadToToeExamination stored = new PhyHeadToToeExamination();
+ stored.setID(2L);
+ when(phyHeadToToeExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(2L, service.savePhyHeadToToeExamination(examination));
+
+ when(phyHeadToToeExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.savePhyHeadToToeExamination(examination));
+ }
+
+ @Test
+ void saveSysGastrointestinalExamination_returnsTheStoredId() {
+ SysGastrointestinalExamination examination = new SysGastrointestinalExamination();
+ SysGastrointestinalExamination stored = new SysGastrointestinalExamination();
+ stored.setID(3L);
+ when(sysGastrointestinalExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(3L, service.saveSysGastrointestinalExamination(examination));
+
+ when(sysGastrointestinalExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysGastrointestinalExamination(examination));
+ }
+
+ @Test
+ void saveSysCardiovascularExamination_returnsTheStoredId() {
+ SysCardiovascularExamination examination = new SysCardiovascularExamination();
+ SysCardiovascularExamination stored = new SysCardiovascularExamination();
+ stored.setID(4L);
+ when(sysCardiovascularExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(4L, service.saveSysCardiovascularExamination(examination));
+
+ when(sysCardiovascularExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysCardiovascularExamination(examination));
+ }
+
+ @Test
+ void saveSysRespiratoryExamination_returnsTheStoredId() {
+ SysRespiratoryExamination examination = new SysRespiratoryExamination();
+ SysRespiratoryExamination stored = new SysRespiratoryExamination();
+ stored.setID(5L);
+ when(sysRespiratoryExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(5L, service.saveSysRespiratoryExamination(examination));
+
+ when(sysRespiratoryExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysRespiratoryExamination(examination));
+ }
+
+ @Test
+ void saveSysCentralNervousExamination_returnsTheStoredId() {
+ SysCentralNervousExamination examination = new SysCentralNervousExamination();
+ SysCentralNervousExamination stored = new SysCentralNervousExamination();
+ stored.setID(6L);
+ when(sysCentralNervousExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(6L, service.saveSysCentralNervousExamination(examination));
+
+ when(sysCentralNervousExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysCentralNervousExamination(examination));
+ }
+
+ @Test
+ void saveSysMusculoskeletalSystemExamination_returnsTheStoredId() {
+ SysMusculoskeletalSystemExamination examination = new SysMusculoskeletalSystemExamination();
+ SysMusculoskeletalSystemExamination stored = new SysMusculoskeletalSystemExamination();
+ stored.setID(7L);
+ when(sysMusculoskeletalSystemExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(7L, service.saveSysMusculoskeletalSystemExamination(examination));
+
+ when(sysMusculoskeletalSystemExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysMusculoskeletalSystemExamination(examination));
+ }
+
+ @Test
+ void saveSysGenitourinarySystemExamination_returnsTheStoredId() {
+ SysGenitourinarySystemExamination examination = new SysGenitourinarySystemExamination();
+ SysGenitourinarySystemExamination stored = new SysGenitourinarySystemExamination();
+ stored.setID(8L);
+ when(sysGenitourinarySystemExaminationRepo.save(examination)).thenReturn(stored);
+ assertEquals(8L, service.saveSysGenitourinarySystemExamination(examination));
+
+ when(sysGenitourinarySystemExaminationRepo.save(examination)).thenReturn(null);
+ assertNull(service.saveSysGenitourinarySystemExamination(examination));
+ }
+ }
+
+ @Nested
+ @DisplayName("beneficiary history tables")
+ class HistoryTables {
+
+ /** A stored row wide enough for any of the history mappers, with no values set. */
+ private ArrayList oneEmptyRow() {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[20]);
+ return rows;
+ }
+
+ private void assertTableWithOneRow(String json) {
+ assertTrue(json.contains("\"columns\""), "the table must describe its columns: " + json);
+ assertTrue(json.contains("\"data\":[{"), "the stored row must be mapped into data: " + json);
+ }
+
+ private void assertEmptyTable(String json) {
+ assertTrue(json.contains("\"columns\""), "the table must describe its columns even when empty: " + json);
+ assertTrue(json.contains("\"data\":[]"), "an empty result must map to no data: " + json);
+ }
+
+ @Test
+ void fetchBenPastMedicalHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benMedHistoryRepo.getBenPastHistory(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPastMedicalHistory(1L));
+
+ when(benMedHistoryRepo.getBenPastHistory(2L)).thenReturn(new ArrayList<>());
+ assertEmptyTable(service.fetchBenPastMedicalHistory(2L));
+
+ when(benMedHistoryRepo.getBenPastHistory(3L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPastMedicalHistory(3L));
+ }
+
+ @Test
+ void fetchBenPersonalTobaccoHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benPersonalHabitRepo.getBenPersonalTobaccoHabitDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPersonalTobaccoHistory(1L));
+
+ when(benPersonalHabitRepo.getBenPersonalTobaccoHabitDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPersonalTobaccoHistory(2L));
+ }
+
+ @Test
+ void fetchBenPersonalAlcoholHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benPersonalHabitRepo.getBenPersonalAlcoholHabitDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPersonalAlcoholHistory(1L));
+
+ when(benPersonalHabitRepo.getBenPersonalAlcoholHabitDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPersonalAlcoholHistory(2L));
+ }
+
+ @Test
+ void fetchBenPersonalAllergyHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPersonalAllergyHistory(1L));
+
+ when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPersonalAllergyHistory(2L));
+ }
+
+ @Test
+ void fetchBenPersonalMedicationHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPersonalMedicationHistory(1L));
+
+ when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPersonalMedicationHistory(2L));
+ }
+
+ @Test
+ void fetchBenPersonalFamilyHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPersonalFamilyHistory(1L));
+
+ when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPersonalFamilyHistory(2L));
+ }
+
+ @Test
+ void fetchBenPhysicalHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(physicalActivityTypeRepo.getBenPhysicalHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPhysicalHistory(1L));
+
+ when(physicalActivityTypeRepo.getBenPhysicalHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPhysicalHistory(2L));
+ }
+
+ @Test
+ void fetchBenMenstrualHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benMenstrualDetailsRepo.getBenMenstrualDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenMenstrualHistory(1L));
+
+ when(benMenstrualDetailsRepo.getBenMenstrualDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenMenstrualHistory(2L));
+ }
+
+ @Test
+ void fetchBenPastObstetricHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(femaleObstetricHistoryRepo.getBenFemaleObstetricHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPastObstetricHistory(1L));
+
+ when(femaleObstetricHistoryRepo.getBenFemaleObstetricHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPastObstetricHistory(2L));
+ }
+
+ @Test
+ void fetchBenComorbidityHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(bencomrbidityCondRepo.getBencomrbidityCondDetails(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenComorbidityHistory(1L));
+
+ when(bencomrbidityCondRepo.getBencomrbidityCondDetails(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenComorbidityHistory(2L));
+ }
+
+ @Test
+ void fetchBenImmunizationHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(childVaccineDetail1Repo.getBenChildVaccineDetails(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenImmunizationHistory(1L));
+
+ when(childVaccineDetail1Repo.getBenChildVaccineDetails(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenImmunizationHistory(2L));
+ }
+
+ @Test
+ void fetchBenOptionalVaccineHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenOptionalVaccineHistory(1L));
+
+ when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenOptionalVaccineHistory(2L));
+ }
+
+ @Test
+ void fetchBenPerinatalHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(perinatalHistoryRepo.getBenPerinatalDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenPerinatalHistory(1L));
+
+ when(perinatalHistoryRepo.getBenPerinatalDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenPerinatalHistory(2L));
+ }
+
+ @Test
+ void fetchBenFeedingHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(childFeedingDetailsRepo.getBenFeedingHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenFeedingHistory(1L));
+
+ when(childFeedingDetailsRepo.getBenFeedingHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenFeedingHistory(2L));
+ }
+
+ @Test
+ void fetchBenDevelopmentHistory_mapsStoredRowsAndTolratesNoHistory() {
+ when(benChildDevelopmentHistoryRepo.getBenDevelopmentHistoryDetail(1L)).thenReturn(oneEmptyRow());
+ assertTableWithOneRow(service.fetchBenDevelopmentHistory(1L));
+
+ when(benChildDevelopmentHistoryRepo.getBenDevelopmentHistoryDetail(2L)).thenReturn(null);
+ assertEmptyTable(service.fetchBenDevelopmentHistory(2L));
+ }
+ }
+
+ @Nested
+ @DisplayName("history updates")
+ class HistoryUpdates {
+
+ /** The (id, processed) pairs the update methods read before deleting the old rows. */
+ private ArrayList statusRows(Object id, String processed) {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[] { id, processed });
+ return rows;
+ }
+
+ @Test
+ void updateBenChiefComplaints_replacesTheExistingComplaints() {
+ BenChiefComplaint complaint = new BenChiefComplaint();
+ complaint.setBeneficiaryRegID(1L);
+ complaint.setVisitCode(2L);
+ complaint.setBenChiefComplaintID(3L);
+ List complaints = Collections.singletonList(complaint);
+ when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(complaints);
+
+ assertEquals(1, service.updateBenChiefComplaints(complaints));
+ verify(benChiefComplaintRepo).deleteExistingBenChiefComplaints(1L, 2L);
+ verify(benChiefComplaintRepo).updateVanSerialNo(3L);
+ }
+
+ @Test
+ void updateBenChiefComplaints_doesNothingWhenNoComplaintWasSent() {
+ assertEquals(0, service.updateBenChiefComplaints(null));
+ assertEquals(0, service.updateBenChiefComplaints(new ArrayList<>()));
+ }
+
+ @Test
+ void updateBenChiefComplaints_reportsFailureWhenNothingWasStored() {
+ BenChiefComplaint complaint = new BenChiefComplaint();
+ List complaints = Collections.singletonList(complaint);
+ when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(new ArrayList<>());
+ assertEquals(0, service.updateBenChiefComplaints(complaints));
+ }
+
+ @Test
+ void updateBenPastHistoryDetails_marksAlreadySyncedRowsAsUpdatedBeforeReplacingThem() throws Exception {
+ BenMedHistory history = mock(BenMedHistory.class);
+ when(history.getBeneficiaryRegID()).thenReturn(1L);
+ when(history.getVisitCode()).thenReturn(2L);
+ when(benMedHistoryRepo.getBenMedHistoryStatus(1L, 2L)).thenReturn(statusRows(5L, "P"));
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory()));
+ when(history.getBenPastHistory()).thenReturn(entries);
+ when(benMedHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenPastHistoryDetails(history));
+ verify(benMedHistoryRepo).deleteExistingBenMedHistory(5L, "U");
+ }
+
+ @Test
+ void updateBenPastHistoryDetails_keepsNeverSyncedRowsMarkedAsNew() throws Exception {
+ BenMedHistory history = mock(BenMedHistory.class);
+ when(benMedHistoryRepo.getBenMedHistoryStatus(any(), any())).thenReturn(statusRows(5L, "N"));
+ when(history.getBenPastHistory()).thenReturn(new ArrayList<>());
+
+ assertEquals(1, service.updateBenPastHistoryDetails(history));
+ verify(benMedHistoryRepo).deleteExistingBenMedHistory(5L, "N");
+ }
+
+ @Test
+ void updateBenPastHistoryDetails_doesNothingWithoutAHistory() throws Exception {
+ assertEquals(0, service.updateBenPastHistoryDetails(null));
+ }
+
+ @Test
+ void updateBenComorbidConditions_replacesTheStoredConditions() {
+ WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class);
+ when(bencomrbidityCondRepo.getBenComrbidityCondHistoryStatus(any(), any()))
+ .thenReturn(statusRows(6L, "P"));
+ ArrayList entries = new ArrayList<>(
+ Collections.singletonList(new BencomrbidityCondDetails()));
+ when(wrapper.getComrbidityConds()).thenReturn(entries);
+ when(bencomrbidityCondRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenComorbidConditions(wrapper));
+ verify(bencomrbidityCondRepo).deleteExistingBenComrbidityCondDetails(6L, "U");
+ }
+
+ @Test
+ void updateBenComorbidConditions_succeedsWhenEveryConditionWasCleared() {
+ WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class);
+ when(bencomrbidityCondRepo.getBenComrbidityCondHistoryStatus(any(), any()))
+ .thenReturn(statusRows(6L, null));
+ when(wrapper.getComrbidityConds()).thenReturn(new ArrayList<>());
+
+ assertEquals(1, service.updateBenComorbidConditions(wrapper));
+ verify(bencomrbidityCondRepo).deleteExistingBenComrbidityCondDetails(6L, "N");
+ }
+
+ @Test
+ void updateBenComorbidConditions_doesNothingWithoutConditions() {
+ assertEquals(0, service.updateBenComorbidConditions(null));
+ }
+
+ @Test
+ void updateBenMedicationHistory_replacesTheStoredMedication() {
+ WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class);
+ when(benMedicationHistoryRepo.getBenMedicationHistoryStatus(any(), any()))
+ .thenReturn(statusRows(7L, "P"));
+ ArrayList entries = new ArrayList<>(
+ Collections.singletonList(new BenMedicationHistory()));
+ when(wrapper.getBenMedicationHistoryDetails()).thenReturn(entries);
+ when(benMedicationHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenMedicationHistory(wrapper));
+ verify(benMedicationHistoryRepo).deleteExistingBenMedicationHistory(7L, "U");
+ }
+
+ @Test
+ void updateBenMedicationHistory_succeedsWhenEveryMedicationWasCleared() {
+ WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class);
+ when(benMedicationHistoryRepo.getBenMedicationHistoryStatus(any(), any()))
+ .thenReturn(statusRows(7L, "N"));
+ when(wrapper.getBenMedicationHistoryDetails()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.updateBenMedicationHistory(wrapper));
+ }
+
+ @Test
+ void updateBenMedicationHistory_doesNothingWithoutMedication() {
+ assertEquals(0, service.updateBenMedicationHistory(null));
+ }
+
+ @Test
+ void updateBenPersonalHistory_replacesTheStoredHabits() {
+ BenPersonalHabit habit = mock(BenPersonalHabit.class);
+ when(benPersonalHabitRepo.getBenPersonalHistoryStatus(any(), any()))
+ .thenReturn(statusRows(Integer.valueOf(8), "P"));
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenPersonalHabit()));
+ when(habit.getPersonalHistory()).thenReturn(entries);
+ when(benPersonalHabitRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenPersonalHistory(habit));
+ verify(benPersonalHabitRepo).deleteExistingBenPersonalHistory(8, "U");
+ }
+
+ @Test
+ void updateBenPersonalHistory_succeedsWhenEveryHabitWasCleared() {
+ BenPersonalHabit habit = mock(BenPersonalHabit.class);
+ when(benPersonalHabitRepo.getBenPersonalHistoryStatus(any(), any()))
+ .thenReturn(statusRows(Integer.valueOf(8), "N"));
+ when(habit.getPersonalHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.updateBenPersonalHistory(habit));
+ }
+
+ @Test
+ void updateBenPersonalHistory_doesNothingWithoutHabits() {
+ assertEquals(0, service.updateBenPersonalHistory(null));
+ }
+
+ @Test
+ void updateBenAllergicHistory_replacesTheStoredAllergies() {
+ BenAllergyHistory allergy = mock(BenAllergyHistory.class);
+ when(benAllergyHistoryRepo.getBenAllergyHistoryStatus(any(), any())).thenReturn(statusRows(9L, "P"));
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenAllergyHistory()));
+ when(allergy.getBenAllergicHistory()).thenReturn(entries);
+ when(benAllergyHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenAllergicHistory(allergy));
+ verify(benAllergyHistoryRepo).deleteExistingBenAllergyHistory(9L, "U");
+ }
+
+ @Test
+ void updateBenAllergicHistory_succeedsWhenEveryAllergyWasCleared() {
+ BenAllergyHistory allergy = mock(BenAllergyHistory.class);
+ when(benAllergyHistoryRepo.getBenAllergyHistoryStatus(any(), any())).thenReturn(statusRows(9L, "N"));
+ when(allergy.getBenAllergicHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.updateBenAllergicHistory(allergy));
+ }
+
+ @Test
+ void updateBenAllergicHistory_doesNothingWithoutAllergies() {
+ assertEquals(0, service.updateBenAllergicHistory(null));
+ }
+
+ @Test
+ void updateBenFamilyHistory_replacesTheStoredFamilyDiseases() {
+ BenFamilyHistory family = mock(BenFamilyHistory.class);
+ when(benFamilyHistoryRepo.getBenFamilyHistoryStatus(any(), any())).thenReturn(statusRows(10L, "P"));
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory()));
+ when(family.getBenFamilyHistory()).thenReturn(entries);
+ when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateBenFamilyHistory(family));
+ verify(benFamilyHistoryRepo).deleteExistingBenFamilyHistory(10L, "U");
+ }
+
+ @Test
+ void updateBenFamilyHistory_succeedsWhenEveryDiseaseWasCleared() {
+ BenFamilyHistory family = mock(BenFamilyHistory.class);
+ when(benFamilyHistoryRepo.getBenFamilyHistoryStatus(any(), any())).thenReturn(statusRows(10L, "N"));
+ when(family.getBenFamilyHistory()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.updateBenFamilyHistory(family));
+ }
+
+ @Test
+ void updateBenFamilyHistory_doesNothingWithoutFamilyHistory() {
+ assertEquals(0, service.updateBenFamilyHistory(null));
+ }
+
+ @Test
+ void updateMenstrualHistory_updatesTheExistingRowWhenOneIsAlreadyStored() {
+ BenMenstrualDetails details = new BenMenstrualDetails();
+ details.setBeneficiaryRegID(1L);
+ details.setVisitCode(2L);
+ ArrayList> problems = new ArrayList<>();
+ Map problem = new HashMap<>();
+ problem.put("menstrualProblemID", 3);
+ problem.put("problemName", "cramps");
+ problems.add(problem);
+ details.setMenstrualProblemList(problems);
+
+ when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(1L, 2L)).thenReturn("P");
+ when(benMenstrualDetailsRepo.updateMenstrualDetails(any(), any(), any(), any(), any(), any(), any(),
+ anyString(), anyString(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong()))
+ .thenReturn(1);
+
+ assertEquals(1, service.updateMenstrualHistory(details));
+ assertEquals("3", details.getMenstrualProblemID());
+ assertEquals("cramps", details.getProblemName());
+ }
+
+ @Test
+ void updateMenstrualHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ BenMenstrualDetails details = new BenMenstrualDetails();
+ details.setModifiedBy("nurse");
+ when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(any(), any())).thenReturn(null);
+
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ stored.setBenMenstrualID(4);
+ when(benMenstrualDetailsRepo.save(details)).thenReturn(stored);
+
+ assertEquals(1, service.updateMenstrualHistory(details));
+ assertEquals("nurse", details.getCreatedBy());
+ }
+
+ @Test
+ void updateMenstrualHistory_reportsFailureWhenTheFreshRowWasNotStored() {
+ BenMenstrualDetails details = new BenMenstrualDetails();
+ when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(any(), any())).thenReturn(null);
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ stored.setBenMenstrualID(0);
+ when(benMenstrualDetailsRepo.save(details)).thenReturn(stored);
+
+ assertEquals(0, service.updateMenstrualHistory(details));
+ }
+
+ @Test
+ void updateMenstrualHistory_doesNothingWithoutMenstrualDetails() {
+ assertEquals(0, service.updateMenstrualHistory(null));
+ }
+
+ @Test
+ void updatePastObstetricHistory_replacesTheStoredPregnancies() {
+ WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class);
+ when(femaleObstetricHistoryRepo.getBenObstetricHistoryStatus(any(), any()))
+ .thenReturn(statusRows(11L, "P"));
+ ArrayList entries = new ArrayList<>(
+ Collections.singletonList(new FemaleObstetricHistory()));
+ when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries);
+ when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updatePastObstetricHistory(wrapper));
+ verify(femaleObstetricHistoryRepo).deleteExistingObstetricHistory(11L, "U");
+ }
+
+ @Test
+ void updatePastObstetricHistory_doesNothingWithoutObstetricHistory() {
+ assertEquals(0, service.updatePastObstetricHistory(null));
+ }
+
+ @Test
+ void updateChildOptionalVaccineDetail_replacesTheStoredOptionalVaccines() {
+ WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class);
+ when(childOptionalVaccineDetailRepo.getBenChildOptionalVaccineHistoryStatus(any(), any()))
+ .thenReturn(statusRows(12L, "P"));
+ ArrayList entries = new ArrayList<>(
+ Collections.singletonList(new ChildOptionalVaccineDetail()));
+ when(wrapper.getChildOptionalVaccineDetails()).thenReturn(entries);
+ when(childOptionalVaccineDetailRepo.saveAll(entries)).thenReturn(entries);
+
+ assertEquals(1, service.updateChildOptionalVaccineDetail(wrapper));
+ verify(childOptionalVaccineDetailRepo).deleteExistingChildOptionalVaccineDetail(12L, "U");
+ }
+
+ @Test
+ void updateChildOptionalVaccineDetail_succeedsWhenEveryOptionalVaccineWasCleared() {
+ WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class);
+ when(childOptionalVaccineDetailRepo.getBenChildOptionalVaccineHistoryStatus(any(), any()))
+ .thenReturn(statusRows(12L, "N"));
+ when(wrapper.getChildOptionalVaccineDetails()).thenReturn(new ArrayList<>());
+ assertEquals(1, service.updateChildOptionalVaccineDetail(wrapper));
+ }
+
+ @Test
+ void updateChildOptionalVaccineDetail_doesNothingWithoutOptionalVaccines() {
+ assertEquals(0, service.updateChildOptionalVaccineDetail(null));
+ }
+
+ @Test
+ void updateChildImmunizationDetail_marksAPreviouslySyncedVaccineAsUpdated() {
+ ChildVaccineDetail1 vaccine = new ChildVaccineDetail1();
+ vaccine.setBeneficiaryRegID(1L);
+ vaccine.setVisitCode(2L);
+ vaccine.setDefaultReceivingAge("6 weeks");
+ vaccine.setVaccineName("BCG");
+
+ WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class);
+ when(wrapper.getBenChildVaccineDetails())
+ .thenReturn(new ArrayList<>(Collections.singletonList(vaccine)));
+
+ ArrayList statuses = new ArrayList<>();
+ statuses.add(new Object[] { "6 weeks", "BCG", "P" });
+ when(childVaccineDetail1Repo.getBenChildVaccineDetailStatus(1L, 2L)).thenReturn(statuses);
+ when(childVaccineDetail1Repo.updateChildANCImmunization(any(), any(), org.mockito.ArgumentMatchers.eq("U"),
+ anyLong(), anyLong(), anyString(), anyString(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateChildImmunizationDetail(wrapper));
+ }
+
+ @Test
+ void updateChildImmunizationDetail_treatsAVaccineWithNoStoredStatusAsNew() {
+ ChildVaccineDetail1 vaccine = new ChildVaccineDetail1();
+ vaccine.setBeneficiaryRegID(1L);
+ vaccine.setVisitCode(2L);
+ vaccine.setDefaultReceivingAge("6 weeks");
+ vaccine.setVaccineName("BCG");
+
+ WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class);
+ when(wrapper.getBenChildVaccineDetails())
+ .thenReturn(new ArrayList<>(Collections.singletonList(vaccine)));
+ when(childVaccineDetail1Repo.getBenChildVaccineDetailStatus(1L, 2L)).thenReturn(new ArrayList<>());
+ when(childVaccineDetail1Repo.updateChildANCImmunization(any(), any(), org.mockito.ArgumentMatchers.eq("N"),
+ anyLong(), anyLong(), anyString(), anyString(), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateChildImmunizationDetail(wrapper));
+ }
+ }
+
+ @Nested
+ @DisplayName("examination updates")
+ class ExaminationUpdates {
+
+ @Test
+ void updatePhyGeneralExamination_joinsDangerSignsAndMarksAnAlreadySyncedRow() {
+ PhyGeneralExamination examination = new PhyGeneralExamination();
+ examination.setBeneficiaryRegID(1L);
+ examination.setVisitCode(2L);
+ examination.setTypeOfDangerSigns(new ArrayList<>(Arrays.asList("fever")));
+ when(phyGeneralExaminationRepo.getBenGeneralExaminationStatus(1L, 2L)).thenReturn("P");
+ when(phyGeneralExaminationRepo.updatePhyGeneralExamination(any(), any(), any(), any(), any(), any(), any(),
+ anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong())).thenReturn(1);
+
+ assertEquals(1, service.updatePhyGeneralExamination(examination));
+ assertEquals("fever,", examination.getTypeOfDangerSign());
+ }
+
+ @Test
+ void updatePhyGeneralExamination_keepsANeverSyncedRowMarkedAsNew() {
+ PhyGeneralExamination examination = new PhyGeneralExamination();
+ when(phyGeneralExaminationRepo.getBenGeneralExaminationStatus(any(), any())).thenReturn("N");
+ when(phyGeneralExaminationRepo.updatePhyGeneralExamination(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ org.mockito.ArgumentMatchers.eq("N"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updatePhyGeneralExamination(examination));
+ }
+
+ @Test
+ void updatePhyGeneralExamination_doesNothingWithoutAnExamination() {
+ assertEquals(0, service.updatePhyGeneralExamination(null));
+ }
+
+ @Test
+ void updatePhyHeadToToeExamination_marksAnAlreadySyncedRowAsUpdated() {
+ PhyHeadToToeExamination examination = new PhyHeadToToeExamination();
+ when(phyHeadToToeExaminationRepo.getBenHeadToToeExaminationStatus(any(), any())).thenReturn("P");
+ when(phyHeadToToeExaminationRepo.updatePhyHeadToToeExamination(any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), any(), any(),
+ org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updatePhyHeadToToeExamination(examination));
+ assertEquals(0, service.updatePhyHeadToToeExamination(null));
+ }
+
+ @Test
+ void updateSysCardiovascularExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysCardiovascularExamination examination = new SysCardiovascularExamination();
+ when(sysCardiovascularExaminationRepo.getBenCardiovascularExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(sysCardiovascularExaminationRepo.updateSysCardiovascularExamination(any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysCardiovascularExamination(examination));
+ assertEquals(0, service.updateSysCardiovascularExamination(null));
+ }
+
+ @Test
+ void updateSysRespiratoryExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysRespiratoryExamination examination = new SysRespiratoryExamination();
+ when(sysRespiratoryExaminationRepo.getBenRespiratoryExaminationStatus(any(), any())).thenReturn("P");
+ when(sysRespiratoryExaminationRepo.updateSysRespiratoryExamination(any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(),
+ any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysRespiratoryExamination(examination));
+ assertEquals(0, service.updateSysRespiratoryExamination(null));
+ }
+
+ @Test
+ void updateSysCentralNervousExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysCentralNervousExamination examination = new SysCentralNervousExamination();
+ when(sysCentralNervousExaminationRepo.getBenCentralNervousExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(sysCentralNervousExaminationRepo.updateSysCentralNervousExamination(any(), any(), any(), any(), any(),
+ any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysCentralNervousExamination(examination));
+ assertEquals(0, service.updateSysCentralNervousExamination(null));
+ }
+
+ @Test
+ void updateSysMusculoskeletalSystemExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysMusculoskeletalSystemExamination examination = new SysMusculoskeletalSystemExamination();
+ when(sysMusculoskeletalSystemExaminationRepo.getBenMusculoskeletalSystemExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(sysMusculoskeletalSystemExaminationRepo.updateSysMusculoskeletalSystemExamination(any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(),
+ any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysMusculoskeletalSystemExamination(examination));
+ assertEquals(0, service.updateSysMusculoskeletalSystemExamination(null));
+ }
+
+ @Test
+ void updateSysGenitourinarySystemExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysGenitourinarySystemExamination examination = new SysGenitourinarySystemExamination();
+ when(sysGenitourinarySystemExaminationRepo.getBenGenitourinarySystemExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(sysGenitourinarySystemExaminationRepo.updateSysGenitourinarySystemExamination(any(), any(), any(),
+ any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysGenitourinarySystemExamination(examination));
+ assertEquals(0, service.updateSysGenitourinarySystemExamination(null));
+ }
+
+ @Test
+ void updateSysGastrointestinalExamination_marksAnAlreadySyncedRowAsUpdated() {
+ SysGastrointestinalExamination examination = new SysGastrointestinalExamination();
+ when(sysGastrointestinalExaminationRepo.getBenGastrointestinalExaminationStatus(any(), any()))
+ .thenReturn("P");
+ when(sysGastrointestinalExaminationRepo.updateSysGastrointestinalExamination(any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(),
+ any())).thenReturn(1);
+
+ assertEquals(1, service.updateSysGastrointestinalExamination(examination));
+ assertEquals(0, service.updateSysGastrointestinalExamination(null));
+ }
+ }
+
+ @Nested
+ @DisplayName("examination and history reads")
+ class Reads {
+
+ private ArrayList oneEmptyRow() {
+ ArrayList rows = new ArrayList<>();
+ rows.add(new Object[20]);
+ return rows;
+ }
+
+ @Test
+ void getBenChiefComplaints_serialisesTheStoredComplaints() {
+ when(benChiefComplaintRepo.getBenChiefComplaints(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getBenChiefComplaints(1L, 2L));
+ }
+
+ @Test
+ void getPastHistoryData_mapsTheStoredRows() {
+ when(benMedHistoryRepo.getBenPastHistory(1L, 2L)).thenReturn(oneEmptyRow());
+ assertNotNull(service.getPastHistoryData(1L, 2L));
+
+ when(benMedHistoryRepo.getBenPastHistory(3L, 4L)).thenReturn(new ArrayList<>());
+ assertNull(service.getPastHistoryData(3L, 4L));
+ }
+
+ @Test
+ void getComorbidityConditionsHistory_mapsTheStoredRows() {
+ when(bencomrbidityCondRepo.getBencomrbidityCondDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getComorbidityConditionsHistory(1L, 2L));
+ }
+
+ @Test
+ void getMedicationHistory_mapsTheStoredRows() {
+ when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getMedicationHistory(1L, 2L));
+ }
+
+ @Test
+ void getPersonalHistory_returnsAnEmptyHabitWhenNothingWasRecorded() {
+ when(benPersonalHabitRepo.getBenPersonalHabitDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getPersonalHistory(1L, 2L));
+ }
+
+ @Test
+ void getPersonalHistory_copiesTheAllergyStatusOntoTheHabit() {
+ when(benPersonalHabitRepo.getBenPersonalHabitDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L, 2L)).thenReturn(oneEmptyRow());
+
+ BenPersonalHabit habit = service.getPersonalHistory(1L, 2L);
+ assertNotNull(habit.getAllergicList());
+ }
+
+ @Test
+ void getFamilyHistory_mapsTheStoredRows() {
+ when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(1L, 2L)).thenReturn(oneEmptyRow());
+ assertNotNull(service.getFamilyHistory(1L, 2L));
+ }
+
+ @Test
+ void getFamilyHistoryDetail_mapsTheScreeningVariantOfTheRows() {
+ when(benFamilyHistoryRepo.getBenFamilyHisDetail(1L, 2L)).thenReturn(oneEmptyRow());
+ assertNotNull(service.getFamilyHistoryDetail(1L, 2L));
+ }
+
+ @Test
+ void getPhysicalActivityType_delegatesToRepo() {
+ PhysicalActivityType stored = new PhysicalActivityType();
+ when(physicalActivityTypeRepo.getBenPhysicalHistoryDetails(1L, 2L)).thenReturn(stored);
+ assertEquals(stored, service.getPhysicalActivityType(1L, 2L));
+ }
+
+ @Test
+ void getBeneficiaryIdrsDetails_mapsTheStoredRows() {
+ when(iDRSDataRepo.getBenIdrsDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getBeneficiaryIdrsDetails(1L, 2L);
+ }
+
+ @Test
+ void getMenstrualHistory_splitsTheStoredProblemsBackIntoAList() {
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ stored.setMenstrualProblemID("1,2");
+ stored.setProblemName("cramps,spotting");
+ try (org.mockito.MockedStatic statics = org.mockito.Mockito
+ .mockStatic(BenMenstrualDetails.class)) {
+ statics.when(() -> BenMenstrualDetails.getBenMenstrualDetails(any())).thenReturn(stored);
+ BenMenstrualDetails result = service.getMenstrualHistory(1L, 2L);
+ assertEquals(2, result.getMenstrualProblemList().size());
+ assertEquals("cramps", result.getMenstrualProblemList().get(0).get("problemName"));
+ }
+ }
+
+ @Test
+ void getMenstrualHistory_leavesTheProblemListUnsetWhenNoProblemWasRecorded() {
+ BenMenstrualDetails stored = new BenMenstrualDetails();
+ try (org.mockito.MockedStatic statics = org.mockito.Mockito
+ .mockStatic(BenMenstrualDetails.class)) {
+ statics.when(() -> BenMenstrualDetails.getBenMenstrualDetails(any())).thenReturn(stored);
+ assertNull(service.getMenstrualHistory(1L, 2L).getMenstrualProblemList());
+ }
+ }
+
+ @Test
+ void getChildOptionalVaccineHistory_mapsTheStoredRows() {
+ when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getChildOptionalVaccineHistory(1L, 2L);
+ }
+
+ @Test
+ void getImmunizationHistory_mapsTheStoredRows() {
+ when(childVaccineDetail1Repo.getBenChildVaccineDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getImmunizationHistory(1L, 2L);
+ }
+
+ @Test
+ void getGeneralExaminationData_splitsTheStoredDangerSignsBackIntoAList() {
+ PhyGeneralExamination stored = new PhyGeneralExamination();
+ stored.setTypeOfDangerSign("fever,bleeding");
+ when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(stored);
+
+ assertEquals(Arrays.asList("fever", "bleeding"), service.getGeneralExaminationData(1L, 2L)
+ .getTypeOfDangerSigns());
+ }
+
+ @Test
+ void getGeneralExaminationData_returnsAnEmptyDangerSignListWhenNoneWereRecorded() {
+ PhyGeneralExamination stored = new PhyGeneralExamination();
+ when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(stored);
+ assertTrue(service.getGeneralExaminationData(1L, 2L).getTypeOfDangerSigns().isEmpty());
+ }
+
+ @Test
+ void getGeneralExaminationData_returnsNullWhenNoExaminationWasRecorded() {
+ when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(null);
+ assertNull(service.getGeneralExaminationData(1L, 2L));
+ }
+
+ @Test
+ void theRemainingSystemExaminationReadsDelegateToTheirRepositories() {
+ PhyHeadToToeExamination headToToe = new PhyHeadToToeExamination();
+ when(phyHeadToToeExaminationRepo.getPhyHeadToToeExaminationData(1L, 2L)).thenReturn(headToToe);
+ assertEquals(headToToe, service.getHeadToToeExaminationData(1L, 2L));
+
+ SysGastrointestinalExamination gastro = new SysGastrointestinalExamination();
+ when(sysGastrointestinalExaminationRepo.getSSysGastrointestinalExamination(1L, 2L)).thenReturn(gastro);
+ assertEquals(gastro, service.getSysGastrointestinalExamination(1L, 2L));
+
+ SysCardiovascularExamination cardio = new SysCardiovascularExamination();
+ when(sysCardiovascularExaminationRepo.getSysCardiovascularExaminationData(1L, 2L)).thenReturn(cardio);
+ assertEquals(cardio, service.getCardiovascularExamination(1L, 2L));
+
+ SysRespiratoryExamination respiratory = new SysRespiratoryExamination();
+ when(sysRespiratoryExaminationRepo.getSysRespiratoryExaminationData(1L, 2L)).thenReturn(respiratory);
+ assertEquals(respiratory, service.getRespiratoryExamination(1L, 2L));
+
+ SysCentralNervousExamination nervous = new SysCentralNervousExamination();
+ when(sysCentralNervousExaminationRepo.getSysCentralNervousExaminationData(1L, 2L)).thenReturn(nervous);
+ assertEquals(nervous, service.getSysCentralNervousExamination(1L, 2L));
+
+ SysMusculoskeletalSystemExamination musculoskeletal = new SysMusculoskeletalSystemExamination();
+ when(sysMusculoskeletalSystemExaminationRepo.getSysMusculoskeletalSystemExamination(1L, 2L))
+ .thenReturn(musculoskeletal);
+ assertEquals(musculoskeletal, service.getMusculoskeletalExamination(1L, 2L));
+
+ SysGenitourinarySystemExamination genitourinary = new SysGenitourinarySystemExamination();
+ when(sysGenitourinarySystemExaminationRepo.getSysGenitourinarySystemExaminationData(1L, 2L))
+ .thenReturn(genitourinary);
+ assertEquals(genitourinary, service.getGenitourinaryExamination(1L, 2L));
+ }
+ }
+
+ @Nested
+ @DisplayName("prescriptions and prescribed drugs")
+ class Prescriptions {
+
+ private com.iemr.mmu.data.snomedct.SCTDescription diagnosis(String term, String conceptId) {
+ com.iemr.mmu.data.snomedct.SCTDescription description = new com.iemr.mmu.data.snomedct.SCTDescription();
+ description.setTerm(term);
+ description.setConceptID(conceptId);
+ return description;
+ }
+
+ private com.iemr.mmu.data.quickConsultation.PrescriptionDetail storedPrescription(Long id) {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail stored =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ stored.setPrescriptionID(id);
+ return stored;
+ }
+
+ @Test
+ void saveBenPrescription_joinsEveryProvisionalDiagnosisTermAndConceptId() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ ArrayList diagnoses = new ArrayList<>();
+ diagnoses.add(diagnosis("Fever", "111"));
+ diagnoses.add(diagnosis("Cough", null));
+ diagnoses.add(diagnosis(null, "333"));
+ prescription.setProvisionalDiagnosisList(diagnoses);
+ when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(21L));
+
+ assertEquals(21L, service.saveBenPrescription(prescription));
+ assertEquals("Fever || Cough", prescription.getDiagnosisProvided());
+ assertEquals("111 || N/A", prescription.getDiagnosisProvided_SCTCode());
+ verify(prescriptionDetailRepo).updateVanSerialNo(21L);
+ }
+
+ @Test
+ void saveBenPrescription_leavesTheDiagnosisUnsetWhenNoneWasProvided() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(22L));
+
+ assertEquals(22L, service.saveBenPrescription(prescription));
+ assertNull(prescription.getDiagnosisProvided());
+ }
+
+ @Test
+ void saveBenPrescription_returnsNullWhenTheRowWasNotPersisted() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(0L));
+ assertNull(service.saveBenPrescription(prescription));
+ }
+
+ @Test
+ void savePrescriptionDetailsAndGetPrescriptionID_buildsThePrescriptionFromTheVisitContext() {
+ when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(23L));
+ ArrayList diagnoses = new ArrayList<>();
+ diagnoses.add(diagnosis("Fever", "111"));
+
+ assertEquals(23L, service.savePrescriptionDetailsAndGetPrescriptionID(1L, 2L, 3, "doctor", "x-ray", 4L, 5,
+ 6, diagnoses));
+ assertEquals(23L, service.savePrescriptionDetailsAndGetPrescriptionID(1L, 2L, 3, "doctor", "x-ray", 4L, 5,
+ 6, null));
+ }
+
+ @Test
+ void savePrescriptionDetailsCovid19_recordsTheDoctorDiagnosisWhenOneWasGiven() {
+ when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(24L));
+
+ assertEquals(24L,
+ service.savePrescriptionDetailsCovid19(1L, 2L, 3, "doctor", "x-ray", 4L, 5, 6, "Covid"));
+ assertEquals(24L, service.savePrescriptionDetailsCovid19(1L, 2L, 3, "doctor", "x-ray", 4L, 5, 6, null));
+ }
+
+ @Test
+ void saveBeneficiaryPrescription_readsThePrescriptionOutOfTheCaseSheet() throws Exception {
+ when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(25L));
+ com.google.gson.JsonObject caseSheet = new com.google.gson.JsonObject();
+ caseSheet.addProperty("beneficiaryRegID", 1);
+ assertEquals(25L, service.saveBeneficiaryPrescription(caseSheet));
+ }
+
+ @Test
+ void updatePrescription_updatesTheStoredRowAndJoinsTheDiagnosisTerms() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ prescription.setBeneficiaryRegID(1L);
+ prescription.setVisitCode(2L);
+ prescription.setPrescriptionID(3L);
+ ArrayList diagnoses = new ArrayList<>();
+ diagnoses.add(diagnosis("Fever", null));
+ diagnoses.add(diagnosis("Cough", "222"));
+ prescription.setProvisionalDiagnosisList(diagnoses);
+
+ when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(1L, 2L, 3L)).thenReturn("P");
+ when(prescriptionDetailRepo.updatePrescription(anyString(), any(), any(),
+ org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong(), anyLong(), any(), anyString(), any()))
+ .thenReturn(1);
+
+ assertEquals(1, service.updatePrescription(prescription));
+ assertEquals("Fever || Cough", prescription.getDiagnosisProvided());
+ assertEquals("N/A || 222", prescription.getDiagnosisProvided_SCTCode());
+ }
+
+ @Test
+ void updatePrescription_insertsAFreshRowWhenNoneIsStoredYet() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(any(), any(), any())).thenReturn(null);
+ when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(26L));
+
+ assertEquals(1, service.updatePrescription(prescription));
+ }
+
+ @Test
+ void updatePrescription_reportsFailureWhenTheFreshRowWasNotStored() {
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(any(), any(), any())).thenReturn(null);
+ when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(0L));
+
+ assertEquals(0, service.updatePrescription(prescription));
+ }
+
+ @Test
+ void saveBeneficiaryLabTestOrderDetails_succeedsWhenTheCaseSheetOrdersNoTest() {
+ assertEquals(1L, service.saveBeneficiaryLabTestOrderDetails(new com.google.gson.JsonObject(), 1L));
+ }
+
+ @Test
+ void saveBeneficiaryLabTestOrderDetails_storesEveryOrderedTest() {
+ com.google.gson.JsonObject caseSheet = new com.google.gson.JsonObject();
+ com.google.gson.JsonArray orders = new com.google.gson.JsonArray();
+ com.google.gson.JsonObject order = new com.google.gson.JsonObject();
+ order.addProperty("testID", 1);
+ orders.add(order);
+ caseSheet.add("labTestOrders", orders);
+
+ when(labTestOrderDetailRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.saveBeneficiaryLabTestOrderDetails(caseSheet, 1L));
+ }
+
+ @Test
+ void saveBenPrescribedDrugsList_calculatesTheQuantityForTabletsAndCapsules() {
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail tablet = drug("Tablet", "One Tab",
+ "Twice Daily(BD)", "5", "Day(s)");
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail syrup = drug("Syrup", "5 ml", "Once Daily(OD)",
+ "5", "Day(s)");
+ tablet.setId(1L);
+ List drugs = Arrays.asList(tablet, syrup);
+ when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs);
+
+ Map result = service.saveBenPrescribedDrugsList(drugs);
+
+ assertEquals(2, result.get("count"));
+ assertEquals(Collections.singletonList(1L), result.get("prescribedDrugIDs"));
+ assertEquals(10, tablet.getQtyPrescribed());
+ assertNull(syrup.getQtyPrescribed());
+ }
+
+ @Test
+ void saveBenPrescribedDrugsList_succeedsWhenNoDrugWasPrescribed() {
+ Map result = service.saveBenPrescribedDrugsList(new ArrayList<>());
+ assertEquals(1, result.get("count"));
+ }
+
+ @Test
+ void saveBenPrescribedDrugsList_reportsNothingSavedWhenTheStoreDropsARow() {
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail tablet = drug("Tablet", "One Tab",
+ "Once Daily(OD)", "1", "Day(s)");
+ List drugs = Collections.singletonList(tablet);
+ when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(new ArrayList<>());
+
+ assertEquals(0, service.saveBenPrescribedDrugsList(drugs).get("count"));
+ }
+
+ @org.junit.jupiter.params.ParameterizedTest(name = "{0} {1} {2} for {3} {4} is {5}")
+ @org.junit.jupiter.params.provider.CsvSource({
+ "Tablet, Half Tab, Once Daily(OD), 2, Day(s), 1",
+ "Tablet, One Tab, Once Daily(OD) Before Food, 2, Day(s), 2",
+ "Tablet, One & Half Tab, Once Daily(OD) After Food, 2, Day(s), 3",
+ "Tablet, Two Tabs, Once Daily(OD) At Bedtime, 2, Day(s), 4",
+ "Capsule, One Cap, Once Daily(OD), 3, Day(s), 3",
+ "Tablet, Half Tab, Twice Daily(BD), 2, Day(s), 2",
+ "Tablet, One & Half Tab, Twice Daily(BD) Before Food, 1, Day(s), 3",
+ "Tablet, Two Tabs, Twice Daily(BD) After Food, 1, Day(s), 4",
+ "Capsule, One Cap, Twice Daily(BD), 1, Day(s), 2",
+ "Tablet, Half Tab, Thrice Daily (TID), 2, Day(s), 3",
+ "Tablet, One Tab, Thrice Daily (TID) After Food, 1, Day(s), 3",
+ "Tablet, One & Half Tab, Thrice Daily (TID) Before Food, 1, Day(s), 5",
+ "Tablet, Two Tabs, Thrice Daily (TID), 1, Day(s), 6",
+ "Capsule, One Cap, Thrice Daily (TID), 1, Day(s), 3",
+ "Tablet, Half Tab, Four Times in a Day (QID), 1, Day(s), 2",
+ "Tablet, One Tab, Four Times in a Day AF, 1, Day(s), 4",
+ "Tablet, One & Half Tab, Four Times in a Day BF, 1, Day(s), 6",
+ "Tablet, Two Tabs, Four Times in a Day (QID), 1, Day(s), 8",
+ "Capsule, One Cap, Four Times in a Day (QID), 1, Day(s), 4",
+ "Tablet, Half Tab, Single Dose, 5, Day(s), 1",
+ "Tablet, One Tab, Stat Dose, 5, Day(s), 1",
+ "Tablet, One & Half Tab, Single Dose Before Food, 1, Day(s), 2",
+ "Tablet, Two Tabs, Single Dose After Food, 1, Day(s), 2",
+ "Capsule, One Cap, Single Dose, 1, Day(s), 1",
+ "Tablet, Half Tab, Once in a Week, 4, Week(s), 2",
+ "Tablet, One Tab, Once in a Week After Food, 4, Week(s), 4",
+ "Tablet, One & Half Tab, Once in a Week Before Food, 4, Week(s), 6",
+ "Tablet, Two Tabs, Once in a Week, 4, Week(s), 8",
+ "Capsule, One Cap, Once in a Week, 4, Week(s), 5",
+ "Tablet, Half Tab, SOS, 2, Day(s), 1",
+ "Tablet, One Tab, SOS, 1, Month(s), 30",
+ "Tablet, One & Half Tab, SOS, 1, Day(s), 2",
+ "Tablet, Two Tabs, SOS, 1, Day(s), 2",
+ "Capsule, One Cap, SOS, 1, Day(s), 1",
+ "Tablet, Unknown Dose, Once Daily(OD), 1, Day(s), 0",
+ "Tablet, One Tab, Unrecognised Frequency, 1, Day(s), 0",
+ "Tablet, One Tab, Once Daily(OD), 1, Unknown Unit, 0" })
+ void saveBenPrescribedDrugsList_derivesTheDispensedQuantityFromFormDoseAndFrequency(String form, String dose,
+ String frequency, String duration, String unit, int expectedQuantity) {
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug = drug(form, dose, frequency, duration,
+ unit);
+ List drugs = Collections.singletonList(drug);
+ when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs);
+
+ service.saveBenPrescribedDrugsList(drugs);
+
+ assertEquals(expectedQuantity, drug.getQtyPrescribed());
+ }
+
+ @Test
+ void saveBenPrescribedDrugsList_leavesTheQuantityAtZeroWhenTheOrderIsIncomplete() {
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug = drug("Tablet", null, "Once Daily(OD)",
+ "1", "Day(s)");
+ List drugs = Collections.singletonList(drug);
+ when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs);
+
+ service.saveBenPrescribedDrugsList(drugs);
+
+ assertEquals(0, drug.getQtyPrescribed());
+ }
+
+ private com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug(String form, String dose,
+ String frequency, String duration, String unit) {
+ com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug =
+ new com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail();
+ drug.setFormName(form);
+ drug.setDose(dose);
+ drug.setFrequency(frequency);
+ drug.setDuration(duration);
+ drug.setUnit(unit);
+ return drug;
+ }
+ }
+
+ @Nested
+ @DisplayName("investigations, worklists and status flags")
+ class WorklistsAndInvestigations {
+
+ @Test
+ void saveBenInvestigationDetails_storesThePrescriptionAndItsInvestigations() {
+ com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper =
+ new com.iemr.mmu.data.anc.WrapperBenInvestigationANC();
+ wrapper.setBeneficiaryRegID(1L);
+ com.iemr.mmu.data.quickConsultation.PrescriptionDetail stored =
+ new com.iemr.mmu.data.quickConsultation.PrescriptionDetail();
+ stored.setPrescriptionID(30L);
+ when(prescriptionDetailRepo.save(any())).thenReturn(stored);
+
+ assertEquals(1, service.saveBenInvestigationDetails(wrapper));
+ assertEquals(30L, wrapper.getPrescriptionID());
+ }
+
+ @Test
+ void saveBenInvestigationDetails_doesNothingWithoutInvestigationData() {
+ assertEquals(0, service.saveBenInvestigationDetails(null));
+ }
+
+ @Test
+ void saveBenInvestigation_copiesTheVisitContextOntoEveryOrderedTest() {
+ com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper =
+ new com.iemr.mmu.data.anc.WrapperBenInvestigationANC();
+ wrapper.setBeneficiaryRegID(1L);
+ wrapper.setBenVisitID(2L);
+ wrapper.setVisitCode(3L);
+ wrapper.setPrescriptionID(4L);
+ com.iemr.mmu.data.quickConsultation.LabTestOrderDetail order =
+ new com.iemr.mmu.data.quickConsultation.LabTestOrderDetail();
+ ArrayList orders = new ArrayList<>(
+ Collections.singletonList(order));
+ wrapper.setLaboratoryList(orders);
+ when(labTestOrderDetailRepo.saveAll(any())).thenAnswer(invocation -> {
+ List> saved = invocation.getArgument(0);
+ return new ArrayList<>(saved);
+ });
+
+ assertEquals(1L, service.saveBenInvestigation(wrapper));
+ assertEquals(4L, order.getPrescriptionID());
+ assertEquals(1L, order.getBeneficiaryRegID());
+ assertEquals(3L, order.getVisitCode());
+ }
+
+ @Test
+ void saveBenInvestigation_succeedsWhenNoTestWasOrdered() {
+ com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper =
+ new com.iemr.mmu.data.anc.WrapperBenInvestigationANC();
+ assertEquals(1L, service.saveBenInvestigation(wrapper));
+ }
+
+ @Test
+ void updateBenVisitStatusFlag_reportsSuccessWhenTheFlagWasStored() {
+ when(benVisitDetailRepo.updateBenFlowStatus("N", 1L)).thenReturn(1);
+ assertTrue(service.updateBenVisitStatusFlag(1L, "N").contains("Updated Successfully"));
+ }
+
+ @Test
+ void updateBenStatus_returnsAnEmptyResultWhenNothingWasUpdated() {
+ when(benVisitDetailRepo.updateBenFlowStatus("N", 1L)).thenReturn(0);
+ assertEquals("{}", service.updateBenStatus(1L, "N"));
+ }
+
+ @Test
+ void getNurseWorkList_serialisesTheRegistrarWorklist() {
+ when(reistrarRepoBenSearch.getNurseWorkList()).thenReturn(new ArrayList<>());
+ assertNotNull(service.getNurseWorkList());
+ }
+
+ @Test
+ void theRoleWorklistsFallBackToASevenDayWindowWhenNoLimitIsConfigured() {
+ when(beneficiaryFlowStatusRepo.getNurseWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getLabWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getRadiologistWorkListNew(any(), any(), any()))
+ .thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getOncologistWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getPharmaWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getNurseWorkListNew(1, 2));
+ assertEquals("[]", service.getLabWorkListNew(1, 2));
+ assertEquals("[]", service.getRadiologistWorkListNew(1, 2));
+ assertEquals("[]", service.getOncologistWorkListNew(1, 2));
+ assertEquals("[]", service.getPharmaWorkListNew(1, 2));
+ }
+
+ @Test
+ void theRoleWorklistsHonourTheConfiguredDayWindow() {
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "nurseWL", 10);
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "labWL", 10);
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "radioWL", 10);
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "oncoWL", 10);
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "pharmaWL", 10);
+ org.springframework.test.util.ReflectionTestUtils.setField(service, "TMReferredWL", 10);
+
+ when(beneficiaryFlowStatusRepo.getNurseWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getNurseWorklistTMreferred(any(), any(), any()))
+ .thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getLabWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getRadiologistWorkListNew(any(), any(), any()))
+ .thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getOncologistWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>());
+ when(beneficiaryFlowStatusRepo.getPharmaWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>());
+
+ assertEquals("[]", service.getNurseWorkListNew(1, 2));
+ assertEquals("[]", service.getNurseWorkListTMReferred(1, 2));
+ assertEquals("[]", service.getLabWorkListNew(1, 2));
+ assertEquals("[]", service.getRadiologistWorkListNew(1, 2));
+ assertEquals("[]", service.getOncologistWorkListNew(1, 2));
+ assertEquals("[]", service.getPharmaWorkListNew(1, 2));
+ }
+ }
+
+ @Nested
+ @DisplayName("child history and screening")
+ class ChildHistoryAndScreening {
+
+ @Test
+ void saveBenAdherenceDetails_reportsWhetherTheRowWasStored() {
+ com.iemr.mmu.data.anc.BenAdherence adherence = new com.iemr.mmu.data.anc.BenAdherence();
+ when(benAdherenceRepo.save(adherence)).thenReturn(adherence);
+ assertEquals(1, service.saveBenAdherenceDetails(adherence));
+
+ when(benAdherenceRepo.save(adherence)).thenReturn(null);
+ assertEquals(0, service.saveBenAdherenceDetails(adherence));
+ }
+
+ @Test
+ void saveChildDevelopmentHistory_returnsTheStoredId() {
+ com.iemr.mmu.data.anc.BenChildDevelopmentHistory history =
+ new com.iemr.mmu.data.anc.BenChildDevelopmentHistory();
+ com.iemr.mmu.data.anc.BenChildDevelopmentHistory stored =
+ new com.iemr.mmu.data.anc.BenChildDevelopmentHistory();
+ stored.setID(31L);
+ when(benChildDevelopmentHistoryRepo.save(any())).thenReturn(stored);
+ assertEquals(31L, service.saveChildDevelopmentHistory(history));
+
+ stored.setID(0L);
+ assertNull(service.saveChildDevelopmentHistory(history));
+ }
+
+ @Test
+ void saveChildFeedingHistory_returnsTheStoredId() {
+ com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails();
+ com.iemr.mmu.data.anc.ChildFeedingDetails stored = new com.iemr.mmu.data.anc.ChildFeedingDetails();
+ stored.setID(32L);
+ when(childFeedingDetailsRepo.save(details)).thenReturn(stored);
+ assertEquals(32L, service.saveChildFeedingHistory(details));
+
+ stored.setID(0L);
+ assertNull(service.saveChildFeedingHistory(details));
+ }
+
+ @Test
+ void savePerinatalHistory_returnsTheStoredId() {
+ com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory();
+ com.iemr.mmu.data.anc.PerinatalHistory stored = new com.iemr.mmu.data.anc.PerinatalHistory();
+ stored.setID(33L);
+ when(perinatalHistoryRepo.save(history)).thenReturn(stored);
+ assertEquals(33L, service.savePerinatalHistory(history));
+
+ stored.setID(0L);
+ assertNull(service.savePerinatalHistory(history));
+ }
+
+ @Test
+ void getBenAdherence_andGetLabTestOrders_serialiseTheStoredRows() {
+ when(benAdherenceRepo.getBenAdherence(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getBenAdherence(1L, 2L));
+
+ when(labTestOrderDetailRepo.getLabTestOrderDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ assertNotNull(service.getLabTestOrders(1L, 2L));
+ }
+
+ @Test
+ void theChildHistoryReadsMapTheStoredRows() {
+ when(benChildDevelopmentHistoryRepo.getBenDevelopmentDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getDevelopmentHistory(1L, 2L);
+
+ when(perinatalHistoryRepo.getBenPerinatalDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getPerinatalHistory(1L, 2L);
+
+ when(childFeedingDetailsRepo.getBenFeedingDetails(1L, 2L)).thenReturn(new ArrayList<>());
+ service.getFeedingHistory(1L, 2L);
+ }
+
+ @Test
+ void updateChildFeedingHistory_updatesTheStoredRowWhenOneExists() {
+ com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails();
+ when(childFeedingDetailsRepo.getBenChildFeedingDetailStatus(any(), any())).thenReturn("P");
+ when(childFeedingDetailsRepo.updateFeedingDetails(any(), any(), any(), any(), any(), any(), any(), any(),
+ org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateChildFeedingHistory(details));
+ }
+
+ @Test
+ void updateChildFeedingHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails();
+ details.setModifiedBy("nurse");
+ com.iemr.mmu.data.anc.ChildFeedingDetails stored = new com.iemr.mmu.data.anc.ChildFeedingDetails();
+ stored.setID(34L);
+ when(childFeedingDetailsRepo.getBenChildFeedingDetailStatus(any(), any())).thenReturn(null);
+ when(childFeedingDetailsRepo.save(details)).thenReturn(stored);
+
+ assertEquals(1, service.updateChildFeedingHistory(details));
+ assertEquals("nurse", details.getCreatedBy());
+ assertEquals(0, service.updateChildFeedingHistory(null));
+ }
+
+ @Test
+ void updatePerinatalHistory_updatesTheStoredRowWhenOneExists() {
+ com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory();
+ when(perinatalHistoryRepo.getPerinatalHistoryStatus(any(), any())).thenReturn("P");
+ when(perinatalHistoryRepo.updatePerinatalDetails(any(), any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updatePerinatalHistory(history));
+ }
+
+ @Test
+ void updatePerinatalHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory();
+ com.iemr.mmu.data.anc.PerinatalHistory stored = new com.iemr.mmu.data.anc.PerinatalHistory();
+ stored.setID(35L);
+ when(perinatalHistoryRepo.getPerinatalHistoryStatus(any(), any())).thenReturn(null);
+ when(perinatalHistoryRepo.save(history)).thenReturn(stored);
+
+ assertEquals(1, service.updatePerinatalHistory(history));
+ assertEquals(0, service.updatePerinatalHistory(null));
+ }
+
+ @Test
+ void updateChildDevelopmentHistory_updatesTheStoredRowWhenOneExists() {
+ com.iemr.mmu.data.anc.BenChildDevelopmentHistory history =
+ new com.iemr.mmu.data.anc.BenChildDevelopmentHistory();
+ when(benChildDevelopmentHistoryRepo.getDevelopmentHistoryStatus(any(), any())).thenReturn("P");
+ when(benChildDevelopmentHistoryRepo.updatePerinatalDetails(any(), any(), any(), any(), any(), any(), any(),
+ any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1);
+
+ assertEquals(1, service.updateChildDevelopmentHistory(history));
+ }
+
+ @Test
+ void updateChildDevelopmentHistory_insertsAFreshRowWhenNoneIsStoredYet() {
+ com.iemr.mmu.data.anc.BenChildDevelopmentHistory history =
+ new com.iemr.mmu.data.anc.BenChildDevelopmentHistory();
+ com.iemr.mmu.data.anc.BenChildDevelopmentHistory stored =
+ new com.iemr.mmu.data.anc.BenChildDevelopmentHistory();
+ stored.setID(36L);
+ when(benChildDevelopmentHistoryRepo.getDevelopmentHistoryStatus(any(), any())).thenReturn(null);
+ when(benChildDevelopmentHistoryRepo.save(any())).thenReturn(stored);
+
+ assertEquals(1, service.updateChildDevelopmentHistory(history));
+ assertEquals(0, service.updateChildDevelopmentHistory(null));
+ }
+
+ @Test
+ void updateBenFamilyHistoryNCDScreening_reportsWhetherEveryDiseaseWasStored() {
+ BenFamilyHistory input = mock(BenFamilyHistory.class);
+ ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory()));
+ when(input.getBenFamilyHist()).thenReturn(entries);
+ when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries);
+ assertEquals(1, service.updateBenFamilyHistoryNCDScreening(input));
+
+ when(input.getBenFamilyHist()).thenReturn(new ArrayList<>());
+ assertEquals(0, service.updateBenFamilyHistoryNCDScreening(input));
+ }
+
+ @Test
+ void updateBenPhysicalActivityHistoryNCDScreening_marksAnExistingRowAsUpdated() {
+ PhysicalActivityType activity = new PhysicalActivityType();
+ activity.setID(1L);
+ when(physicalActivityTypeRepo.save(activity)).thenReturn(activity);
+
+ assertEquals(1, service.updateBenPhysicalActivityHistoryNCDScreening(activity));
+ assertEquals("U", activity.getProcessed());
+ assertEquals(Boolean.FALSE, activity.getDeleted());
+ }
+
+ @Test
+ void updateBenPhysicalActivityHistoryNCDScreening_marksAFreshRowAsNew() {
+ PhysicalActivityType activity = new PhysicalActivityType();
+ when(physicalActivityTypeRepo.save(activity)).thenReturn(null);
+
+ assertEquals(0, service.updateBenPhysicalActivityHistoryNCDScreening(activity));
+ assertEquals("N", activity.getProcessed());
+ }
+ }
+
+ @Nested
+ @DisplayName("graph trends and NCD summaries")
+ class TrendsAndSummaries {
+
+ @Test
+ void getGraphicalTrendData_readsWeightAndVitalsFromTheNonCancerVisits() {
+ ArrayList visits = new ArrayList<>();
+ visits.add(new Object[] { 1L, "ANC", 100L });
+ visits.add(new Object[] { 2L, "ANC", null });
+ when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(visits);
+
+ ArrayList anthro = new ArrayList<>();
+ anthro.add(new Object[] { 55.5d, Date.valueOf("2024-01-01") });
+ when(benAnthropometryRepo.getBenAnthropometryDetailForGraphtrends(any())).thenReturn(anthro);
+
+ ArrayList vitals = new ArrayList<>();
+ vitals.add(new Object[] { (short) 120, (short) 80, 90d, 140d, 150d, Date.valueOf("2024-01-01") });
+ vitals.add(new Object[] { (short) 0, (short) 0, null, null, null, Date.valueOf("2024-01-02") });
+ when(benPhysicalVitalRepo.getBenPhysicalVitalDetailForGraphTrends(any())).thenReturn(vitals);
+
+ Map trends = service.getGraphicalTrendData(1L, "ANC");
+
+ assertEquals(1, ((List>) trends.get("weightList")).size());
+ assertEquals(1, ((List>) trends.get("bpList")).size());
+ assertEquals(1, ((List>) trends.get("bgList")).size());
+ }
+
+ @Test
+ void getGraphicalTrendData_averagesTheThreeReadingsTakenAtCancerScreeningVisits() {
+ ArrayList visits = new ArrayList<>();
+ visits.add(new Object[] { 1L, "Cancer Screening", 100L });
+ when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(visits);
+
+ com.iemr.mmu.data.nurse.BenCancerVitalDetail cancerVital =
+ new com.iemr.mmu.data.nurse.BenCancerVitalDetail();
+ cancerVital.setWeight_Kg(60d);
+ cancerVital.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis()));
+ cancerVital.setSystolicBP_1stReading((short) 120);
+ cancerVital.setSystolicBP_2ndReading((short) 130);
+ cancerVital.setSystolicBP_3rdReading((short) 140);
+ cancerVital.setDiastolicBP_1stReading((short) 80);
+ cancerVital.setDiastolicBP_2ndReading((short) 90);
+ cancerVital.setDiastolicBP_3rdReading((short) 100);
+ cancerVital.setBloodGlucose_Fasting((short) 95);
+
+ ArrayList cancerVitals = new ArrayList<>(
+ Collections.singletonList(cancerVital));
+ when(benCancerVitalDetailRepo.getBenCancerVitalDetailForGraph(any())).thenReturn(cancerVitals);
+
+ Map trends = service.getGraphicalTrendData(1L, "Cancer Screening");
+
+ assertEquals(1, ((List>) trends.get("weightList")).size());
+ List> bpList = (List>) trends.get("bpList");
+ assertEquals(1, bpList.size());
+ assertEquals(130, ((Map, ?>) bpList.get(0)).get("avgSysBP"));
+ assertEquals(90, ((Map, ?>) bpList.get(0)).get("avgDysBP"));
+ assertEquals(1, ((List>) trends.get("bgList")).size());
+ }
+
+ @Test
+ void getGraphicalTrendData_returnsEmptyTrendsWhenTheBeneficiaryHasNoVisits() {
+ when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(new ArrayList<>());
+ Map trends = service.getGraphicalTrendData(1L, "ANC");
+ assertTrue(((List>) trends.get("weightList")).isEmpty());
+ }
+
+ @Test
+ void getBenSymptomaticData_collectsTheAnswersOfTheMostRecentVisit() throws Exception {
+ IDRSData first = new IDRSData();
+ first.setVisitCode(1L);
+ first.setIdrsQuestionID(10);
+ first.setAnswer("Yes");
+ first.setSuspectedDisease("Diabetes");
+ first.setConfirmedDisease("None");
+ IDRSData sameVisit = new IDRSData();
+ sameVisit.setVisitCode(1L);
+ sameVisit.setIdrsQuestionID(11);
+ IDRSData earlierVisit = new IDRSData();
+ earlierVisit.setVisitCode(2L);
+
+ when(iDRSDataRepo.getBenIdrsDetailsLast_3_Month(anyLong(), any()))
+ .thenReturn(new ArrayList<>(Arrays.asList(first, sameVisit, earlierVisit)));
+ when(iDRSDataRepo.isDiabeticCheck(1L)).thenReturn(1);
+ when(iDRSDataRepo.isHypertensionCheck(1L)).thenReturn(1);
+ when(iDRSDataRepo.isEpilepsyCheck(1L)).thenReturn(1);
+ when(iDRSDataRepo.isDefectiveVisionCheck(1L)).thenReturn(1);
+
+ String json = service.getBenSymptomaticData(1L);
+
+ assertTrue(json.contains("\"isDiabetic\":true"), json);
+ assertTrue(json.contains("\"isHypertension\":true"), json);
+ assertTrue(json.contains("\"isEpilepsy\":true"), json);
+ assertTrue(json.contains("\"isDefectiveVision\":true"), json);
+ assertTrue(json.contains("\"suspectedDisease\":\"Diabetes\""), json);
+ assertTrue(json.contains("\"confirmedDisease\":\"None\""), json);
+ }
+
+ @Test
+ void getBenSymptomaticData_reportsNoConditionsWhenNothingWasScreened() throws Exception {
+ when(iDRSDataRepo.getBenIdrsDetailsLast_3_Month(anyLong(), any())).thenReturn(new ArrayList<>());
+ when(iDRSDataRepo.isDiabeticCheck(1L)).thenReturn(0);
+ when(iDRSDataRepo.isHypertensionCheck(1L)).thenReturn(0);
+ when(iDRSDataRepo.isEpilepsyCheck(1L)).thenReturn(null);
+ when(iDRSDataRepo.isDefectiveVisionCheck(1L)).thenReturn(null);
+
+ String json = service.getBenSymptomaticData(1L);
+
+ assertTrue(json.contains("\"isDiabetic\":false"), json);
+ assertTrue(json.contains("\"isEpilepsy\":false"), json);
+ assertTrue(json.contains("\"questionariesData\":[]"), json);
+ }
+
+ @Test
+ void getBenPreviousDiabetesData_keepsOnlyTheDiabetesQuestionsOfEachRow() throws Exception {
+ IDRSData diabetesRow = new IDRSData();
+ diabetesRow.setDiseaseQuestionType("Diabetes||Hypertension");
+ diabetesRow.setAnswer("Yes||No");
+ diabetesRow.setQuestion("Do you have diabetes?||Do you have hypertension?");
+ diabetesRow.setQuestionIds("1||2");
+ IDRSData otherRow = new IDRSData();
+ otherRow.setDiseaseQuestionType("Hypertension");
+ otherRow.setAnswer("No");
+ otherRow.setQuestion("Do you have hypertension?");
+ otherRow.setQuestionIds("2");
+
+ when(iDRSDataRepo.getBenPreviousDiabetesDetails(1L))
+ .thenReturn(new ArrayList<>(Arrays.asList(diabetesRow, otherRow)));
+
+ String json = service.getBenPreviousDiabetesData(1L);
+
+ assertTrue(json.contains("Do you have diabetes?"), json);
+ assertTrue(json.contains("\"columns\""), json);
+ }
+
+ @Test
+ void getBenPreviousReferralData_mapsEachStoredReferral() throws Exception {
+ ArrayList referrals = new ArrayList<>();
+ referrals.add(new Object[] { java.math.BigInteger.valueOf(5),
+ new java.sql.Timestamp(System.currentTimeMillis()), "Diabetes" });
+ when(iDRSDataRepo.getBenPreviousReferredDetails(1L)).thenReturn(referrals);
+
+ String json = service.getBenPreviousReferralData(1L);
+ assertTrue(json.contains("Diabetes"), json);
+
+ when(iDRSDataRepo.getBenPreviousReferredDetails(2L)).thenReturn(null);
+ assertTrue(service.getBenPreviousReferralData(2L).contains("\"data\":[]"));
+ }
+
+ @org.junit.jupiter.params.ParameterizedTest(name = "a BMI of {0} reads as {1}")
+ @org.junit.jupiter.params.provider.CsvSource({ "0.5, Normal", "-1.5, Mild malnourished",
+ "-2.5, Moderately Malnourished", "-4.0, Severely Malnourished", "1.5, Overweight", "2.5, Obese",
+ "3.5, Severely Obese" })
+ void calculateBMIStatus_classifiesTheBmiAgainstTheStoredStandardDeviations(double bmi, String expectedStatus)
+ throws Exception {
+ com.iemr.mmu.data.bmi.BmiCalculation standard = new com.iemr.mmu.data.bmi.BmiCalculation();
+ standard.setN3SD(-3d);
+ standard.setN2SD(-2d);
+ standard.setN1SD(-1d);
+ standard.setP1SD(1d);
+ standard.setP2SD(2d);
+ standard.setP3SD(3d);
+ when(bmiCalculationRepo.getBMIDetails(27, "Male")).thenReturn(standard);
+
+ String request = "{\"yearMonth\":\"2 Years and 3 Months\",\"gender\":\"Male\",\"bmi\":" + bmi + "}";
+ assertTrue(service.calculateBMIStatus(request).contains(expectedStatus));
+ }
+
+ @Test
+ void calculateBMIStatus_reportsNoStatusWhenTheRequestIsIncomplete() throws Exception {
+ assertEquals("{\"bmiStatus\":\"\"}", service.calculateBMIStatus("{}"));
+ }
+
+ @Test
+ void calculateBMIStatus_failsWhenNoStandardIsStoredForTheCategory() {
+ when(bmiCalculationRepo.getBMIDetails(anyInt(), anyString())).thenReturn(null);
+ String request = "{\"yearMonth\":\"2 Years and 3 Months\",\"gender\":\"Male\",\"bmi\":1.0}";
+ IEMRException thrown = assertThrows(IEMRException.class, () -> service.calculateBMIStatus(request));
+ assertTrue(thrown.getMessage().contains("No data found for this category"));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java
new file mode 100644
index 00000000..c571661c
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java
@@ -0,0 +1,560 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.common.transaction;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus;
+import com.iemr.mmu.data.syncActivity_syncLayer.EmployeeSignature;
+import com.iemr.mmu.data.syncActivity_syncLayer.DownloadedCaseSheet;
+import com.iemr.mmu.data.common.DocFileManager;
+import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.EmployeeSignatureRepo;
+import com.iemr.mmu.repo.nurse.ncdscreening.IDRSDataRepo;
+import com.iemr.mmu.repo.provider.ProviderServiceMappingRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.DownloadedCaseSheetRepo;
+import com.iemr.mmu.service.anc.ANCServiceImpl;
+import com.iemr.mmu.service.cancerScreening.CSNurseServiceImpl;
+import com.iemr.mmu.service.cancerScreening.CSServiceImpl;
+import com.iemr.mmu.service.covid19.Covid19ServiceImpl;
+import com.iemr.mmu.service.generalOPD.GeneralOPDServiceImpl;
+import com.iemr.mmu.service.ncdCare.NCDCareServiceImpl;
+import com.iemr.mmu.service.ncdscreening.NCDScreeningServiceImpl;
+import com.iemr.mmu.service.pnc.PNCServiceImpl;
+import com.iemr.mmu.service.quickConsultation.QuickConsultationServiceImpl;
+import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption;
+import com.iemr.mmu.utils.CookieUtil;
+import com.iemr.mmu.utils.exception.IEMRException;
+
+class CommonServiceImplTest {
+
+ @Mock
+ private Covid19ServiceImpl covid19ServiceImpl;
+ @Mock
+ private AESEncryptionDecryption aESEncryptionDecryption;
+ @Mock
+ private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo;
+ @Mock
+ private ANCServiceImpl ancServiceImpl;
+ @Mock
+ private PNCServiceImpl pncServiceImpl;
+ @Mock
+ private GeneralOPDServiceImpl generalOPDServiceImpl;
+ @Mock
+ private NCDCareServiceImpl ncdCareServiceImpl;
+ @Mock
+ private QuickConsultationServiceImpl quickConsultationServiceImpl;
+ @Mock
+ private CommonNurseServiceImpl commonNurseServiceImpl;
+ @Mock
+ private CSNurseServiceImpl cSNurseServiceImpl;
+ @Mock
+ private CSServiceImpl csServiceImpl;
+ @Mock
+ private NCDScreeningServiceImpl ncdScreeningServiceImpl;
+ @Mock
+ private ProviderServiceMappingRepo providerServiceMappingRepo;
+ @Mock
+ private DownloadedCaseSheetRepo downloadedCaseSheetRepo;
+ @Mock
+ private IDRSDataRepo iDRSDataRepo;
+ @Mock
+ private EmployeeSignatureRepo employeeSignatureRepo;
+ @Mock
+ private CookieUtil cookieUtil;
+
+ @InjectMocks
+ private CommonServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "mmuCentralServer", "http://central/casesheet");
+ ReflectionTestUtils.setField(service, "tmCentralServer", "http://central/tm");
+ ReflectionTestUtils.setField(service, "specialistSign", "http://central/sign");
+ }
+
+ @AfterEach
+ void clearRequestContext() {
+ RequestContextHolder.resetRequestAttributes();
+ }
+
+ private static BeneficiaryFlowStatus flow(String visitCategory) {
+ BeneficiaryFlowStatus flow = new BeneficiaryFlowStatus();
+ flow.setVisitCategory(visitCategory);
+ flow.setBeneficiaryRegID(1L);
+ flow.setBenVisitCode(2L);
+ flow.setVisitCode(2L);
+ flow.setBenFlowID(3L);
+ flow.setBenVisitID(4L);
+ return flow;
+ }
+
+ @Nested
+ @DisplayName("case sheet print data")
+ class PrintData {
+
+ @BeforeEach
+ void stubTheLeftPanel() {
+ when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(new ArrayList<>());
+ }
+
+ @Test
+ void getCaseSheetPrintDataForBeneficiary_routesEachVisitCategoryToItsOwnPrintData() throws Exception {
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("ANC"), "auth").contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("PNC"), "auth").contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("General OPD"), "auth")
+ .contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("NCD care"), "auth").contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("General OPD (QC)"), "auth")
+ .contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("COVID-19 Screening"), "auth")
+ .contains("nurseData"));
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("NCD screening"), "auth")
+ .contains("nurseData"));
+ }
+
+ @Test
+ void getCaseSheetPrintDataForBeneficiary_addsTheAnnotatedImagesForCancerScreening() throws Exception {
+ when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L))
+ .thenReturn(new ArrayList<>());
+
+ assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("Cancer Screening"), "auth")
+ .contains("ImageAnnotatedData"));
+ }
+
+ @Test
+ void getCaseSheetPrintDataForBeneficiary_rejectsAnUnknownVisitCategory() throws Exception {
+ assertEquals("Invalid VisitCategory",
+ service.getCaseSheetPrintDataForBeneficiary(flow("Unknown"), "auth"));
+ }
+ }
+
+ @Nested
+ @DisplayName("past history reads")
+ class PastHistoryReads {
+
+ @Test
+ void everyPastHistoryReadDelegatesToTheNurseService() throws Exception {
+ when(commonNurseServiceImpl.fetchBenPastMedicalHistory(1L)).thenReturn("past");
+ assertEquals("past", service.getBenPastHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenComorbidityHistory(1L)).thenReturn("comorbid");
+ assertEquals("comorbid", service.getComorbidHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPersonalMedicationHistory(1L)).thenReturn("medication");
+ assertEquals("medication", service.getMedicationHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPersonalTobaccoHistory(1L)).thenReturn("tobacco");
+ assertEquals("tobacco", service.getPersonalTobaccoHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPersonalAlcoholHistory(1L)).thenReturn("alcohol");
+ assertEquals("alcohol", service.getPersonalAlcoholHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPersonalAllergyHistory(1L)).thenReturn("allergy");
+ assertEquals("allergy", service.getPersonalAllergyHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPersonalFamilyHistory(1L)).thenReturn("family");
+ assertEquals("family", service.getFamilyHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPhysicalHistory(1L)).thenReturn("physical");
+ assertEquals("physical", service.getBenPhysicalHistory(1L));
+
+ when(commonNurseServiceImpl.fetchBenMenstrualHistory(1L)).thenReturn("menstrual");
+ assertEquals("menstrual", service.getMenstrualHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPastObstetricHistory(1L)).thenReturn("obstetric");
+ assertEquals("obstetric", service.getObstetricHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenImmunizationHistory(1L)).thenReturn("immunization");
+ assertEquals("immunization", service.getImmunizationHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenOptionalVaccineHistory(1L)).thenReturn("vaccine");
+ assertEquals("vaccine", service.getChildVaccineHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenPerinatalHistory(1L)).thenReturn("perinatal");
+ assertEquals("perinatal", service.getBenPerinatalHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenFeedingHistory(1L)).thenReturn("feeding");
+ assertEquals("feeding", service.getBenFeedingHistoryData(1L));
+
+ when(commonNurseServiceImpl.fetchBenDevelopmentHistory(1L)).thenReturn("development");
+ assertEquals("development", service.getBenDevelopmentHistoryData(1L));
+
+ when(commonNurseServiceImpl.getBenSymptomaticData(1L)).thenReturn("symptomatic");
+ assertEquals("symptomatic", service.getBenSymptomaticQuestionnaireDetailsData(1L));
+
+ when(commonNurseServiceImpl.getBenPreviousDiabetesData(1L)).thenReturn("diabetes");
+ assertEquals("diabetes", service.getBenPreviousDiabetesData(1L));
+
+ when(commonNurseServiceImpl.getBenPreviousReferralData(1L)).thenReturn("referral");
+ assertEquals("referral", service.getBenPreviousReferralData(1L));
+ }
+
+ @Test
+ void getBenPreviousVisitDataForCaseRecord_looksTheVisitsUpAgainstEveryMmuProvider() throws Exception {
+ when(providerServiceMappingRepo.getProviderServiceMapIdForServiceID((short) 2))
+ .thenReturn(new ArrayList<>(Collections.singletonList(5)));
+ when(beneficiaryFlowStatusRepo.getBenPreviousHistory(eq(1L), any())).thenReturn(new ArrayList<>());
+
+ assertNotNull(service.getBenPreviousVisitDataForCaseRecord("{\"beneficiaryRegID\":1}"));
+ }
+ }
+
+ @Nested
+ @DisplayName("uploaded files")
+ class Files {
+
+ @TempDir
+ Path uploadRoot;
+
+ @Test
+ void saveFiles_writesEachAttachmentUnderTheVanAndDateFolderAndEncryptsItsPath() throws Exception {
+ ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/");
+ when(aESEncryptionDecryption.encrypt(anyString())).thenReturn("encrypted");
+
+ DocFileManager attachment = new DocFileManager();
+ attachment.setVanID(7);
+ attachment.setFileName("re*port.pdf");
+ attachment.setFileExtension(".pdf");
+ attachment.setFileContent(Base64.getEncoder().encodeToString("content".getBytes()));
+
+ String response = service.saveFiles(Collections.singletonList(attachment));
+
+ assertTrue(response.contains("encrypted"));
+ assertTrue(response.contains("report.pdf"));
+ }
+
+ @Test
+ void saveFiles_reusesTheDateFolderOnASecondUpload() throws Exception {
+ ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/");
+ when(aESEncryptionDecryption.encrypt(anyString())).thenReturn("encrypted");
+
+ DocFileManager attachment = new DocFileManager();
+ attachment.setVanID(7);
+ attachment.setFileName("report.pdf");
+ attachment.setFileExtension(".pdf");
+ attachment.setFileContent(Base64.getEncoder().encodeToString("content".getBytes()));
+
+ service.saveFiles(Collections.singletonList(attachment));
+ assertTrue(service.saveFiles(Collections.singletonList(attachment)).contains("encrypted"));
+ }
+
+ @Test
+ void saveFiles_skipsAnAttachmentWithoutANameOrExtension() throws Exception {
+ ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/");
+
+ DocFileManager attachment = new DocFileManager();
+ attachment.setVanID(7);
+
+ assertEquals("[]", service.saveFiles(Collections.singletonList(attachment)));
+ }
+
+ @Test
+ void saveFiles_returnsNothingWhenNoAttachmentWasSent() throws Exception {
+ ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/");
+ assertEquals("[]", service.saveFiles(new ArrayList<>()));
+ }
+
+ @Test
+ void loadFileAsResource_returnsAnExistingFile() throws Exception {
+ Path file = uploadRoot.resolve("report.pdf");
+ java.nio.file.Files.write(file, "content".getBytes());
+
+ assertNotNull(service.loadFileAsResource("report.pdf", file.toString()));
+ }
+
+ @Test
+ void loadFileAsResource_failsForAMissingFile() {
+ assertThrows(IOException.class,
+ () -> service.loadFileAsResource("gone.pdf", uploadRoot.resolve("gone.pdf").toString()));
+ }
+ }
+
+ @Nested
+ @DisplayName("teleconsultation case sheets")
+ class TeleconsultationCaseSheets {
+
+ private CommonServiceImpl serviceSpy;
+
+ @BeforeEach
+ void useASpyForTheOutboundCalls() {
+ serviceSpy = spy(service);
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
+ }
+
+ private ResponseEntity ok(String body) {
+ return new ResponseEntity<>(body, HttpStatus.OK);
+ }
+
+ @Test
+ void checkIsCaseSheetDownloaded_reportsWhetherTheSheetWasAlreadyPulled() throws Exception {
+ when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(true);
+ assertEquals(1, service.checkIsCaseSheetDownloaded(1L));
+
+ when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(false);
+ assertEquals(0, service.checkIsCaseSheetDownloaded(1L));
+
+ when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(null);
+ assertEquals(0, service.checkIsCaseSheetDownloaded(1L));
+ }
+
+ @Test
+ void getTmVisitCode_readsTheTeleconsultationVisitOfAnMmuVisit() throws Exception {
+ BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus();
+ when(beneficiaryFlowStatusRepo.getTMVisitDetails(1L)).thenReturn(tmVisit);
+ assertEquals(tmVisit, service.getTmVisitCode(1L));
+ }
+
+ @Test
+ void getTmCaseSheet_returnsTheCaseSheetAndTheSpecialistSignature() throws Exception {
+ String caseSheet = "{\"statusCode\":200,\"data\":{\"BeneficiaryData\":{\"tCSpecialistUserID\":5}}}";
+ String signature = "{\"statusCode\":200,\"data\":{\"userID\":5}}";
+ doReturn(ok(caseSheet)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any());
+ doReturn(ok(signature)).when(serviceSpy).restTemplateGet(anyString(), any(), any());
+
+ ArrayList result = serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth");
+
+ assertEquals(2, result.size());
+ }
+
+ @Test
+ void getTmCaseSheet_returnsOnlyTheCaseSheetWhenNoSpecialistSigned() throws Exception {
+ String caseSheet = "{\"statusCode\":200,\"data\":{\"BeneficiaryData\":{}}}";
+ doReturn(ok(caseSheet)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any());
+
+ assertEquals(1,
+ serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth").size());
+ }
+
+ @Test
+ void getTmCaseSheet_reportsTheLoginFailureFromTheCentralServer() {
+ String error = "{\"statusCode\":5002,\"errorMessage\":\"Session expired\"}";
+ doReturn(ok(error)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any());
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth"));
+ assertEquals(5002, thrown.getErrorCode());
+ }
+
+ @Test
+ void getTmCaseSheet_reportsAnyOtherFailureFromTheCentralServer() {
+ String error = "{\"statusCode\":5000,\"errorMessage\":\"Boom\"}";
+ doReturn(ok(error)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any());
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth"));
+ assertEquals("Boom", thrown.getMessage());
+ }
+
+ @Test
+ void getTmCaseSheet_failsWhenTheCentralServerRejectsTheRequest() {
+ doReturn(new ResponseEntity(HttpStatus.BAD_REQUEST)).when(serviceSpy)
+ .restTemplatePost(anyString(), any(), anyString(), any());
+
+ assertThrows(IEMRException.class,
+ () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth"));
+ }
+
+ @Test
+ void getTmCaseSheetOffline_returnsThePreviouslyDownloadedSheet() throws Exception {
+ DownloadedCaseSheet stored = new DownloadedCaseSheet();
+ stored.setTmCaseSheetResponse("sheet");
+ when(downloadedCaseSheetRepo.getTmCaseSheetFromOffline(2L)).thenReturn(stored);
+
+ assertEquals("sheet", service.getTmCaseSheetOffline(flow("NCD screening")));
+ }
+
+ @Test
+ void getTmCaseSheetOffline_failsWhenNothingWasDownloadedYet() {
+ when(downloadedCaseSheetRepo.getTmCaseSheetFromOffline(2L)).thenReturn(null);
+
+ assertThrows(IEMRException.class, () -> service.getTmCaseSheetOffline(flow("NCD screening")));
+ }
+
+ @Test
+ void getCaseSheetOfTm_returnsTheCaseSheetOnceTeleconsultationIsComplete() throws Exception {
+ BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus();
+ tmVisit.setSpecialist_flag((short) 9);
+ when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(tmVisit);
+ doReturn(new ArrayList<>(Collections.singletonList("sheet"))).when(serviceSpy).getTmCaseSheet(any(),
+ any(), anyString());
+
+ assertTrue(serviceSpy.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth").contains("sheet"));
+ }
+
+ @Test
+ void getCaseSheetOfTm_failsWhileTeleconsultationIsStillPending() {
+ BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus();
+ tmVisit.setSpecialist_flag((short) 1);
+ when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(tmVisit);
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> service.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth"));
+ assertEquals("Tele-Consultation is not completed", thrown.getMessage());
+ }
+
+ @Test
+ void getCaseSheetOfTm_failsWhileTheBeneficiaryIsStillInTheTeleconsultationWorklist() {
+ when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(null);
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> service.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth"));
+ assertEquals("Patient is waiting in Tele-Medicine worklist", thrown.getMessage());
+ }
+
+ /** The central-server payload for a downloaded case sheet, with an optional signature. */
+ private String centralPayload(boolean withSignature) {
+ String caseSheet = "{\"nurseData\":{\"history\":{\"PhysicalActivityHistory\":{\"visitCode\":9,"
+ + "\"createdBy\":\"specialist\"}},\"idrs\":{\"IDRSDetail\":{\"confirmedDisease\":\"Diabetes\","
+ + "\"suspectedDisease\":\"Hypertension\"}}}}";
+ String signature = withSignature ? ",{\"userID\":5}" : "";
+ return "{\"statusCode\":200,\"data\":[" + caseSheet + signature + "]}";
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_storesTheSheetTheSignatureAndTheScreeningOutcome() throws Exception {
+ doReturn(ok(centralPayload(true))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(),
+ any());
+ when(employeeSignatureRepo.findOneByUserID(5L)).thenReturn(null);
+ when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet());
+ when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1);
+ when(iDRSDataRepo.updateConfirmedAndSuspectedDisease("Diabetes", "Hypertension", 2L)).thenReturn(1);
+
+ assertTrue(serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")
+ .contains("PhysicalActivityHistory"));
+ verify(employeeSignatureRepo).save(any());
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_keepsAnAlreadyStoredSpecialistSignature() throws Exception {
+ doReturn(ok(centralPayload(true))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(),
+ any());
+ when(employeeSignatureRepo.findOneByUserID(5L)).thenReturn(new EmployeeSignature());
+ when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet());
+ when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1);
+ when(iDRSDataRepo.updateConfirmedAndSuspectedDisease(any(), any(), anyLong())).thenReturn(1);
+
+ assertNotNull(serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ verify(employeeSignatureRepo, org.mockito.Mockito.never()).save(any());
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_failsWhenTheScreeningOutcomeCouldNotBeStored() throws Exception {
+ doReturn(ok(centralPayload(false))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(),
+ any());
+ when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet());
+ when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1);
+ when(iDRSDataRepo.updateConfirmedAndSuspectedDisease(any(), any(), anyLong())).thenReturn(0);
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ assertTrue(thrown.getMessage().contains("confirmed and suspected disease"));
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_failsWhenTheDownloadFlagCouldNotBeStored() throws Exception {
+ doReturn(ok(centralPayload(false))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(),
+ any());
+ when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet());
+ when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(0);
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ assertTrue(thrown.getMessage().contains("download flag"));
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_reportsTheLoginFailureFromTheCentralServer() {
+ doReturn(ok("{\"statusCode\":5002,\"errorMessage\":\"Session expired\"}")).when(serviceSpy)
+ .restTemplatePost(anyString(), any(), anyString(), any());
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ assertEquals(5002, thrown.getErrorCode());
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_reportsAnyOtherFailureFromTheCentralServer() {
+ doReturn(ok("{\"statusCode\":5000,\"errorMessage\":\"Boom\"}")).when(serviceSpy)
+ .restTemplatePost(anyString(), any(), anyString(), any());
+
+ IEMRException thrown = assertThrows(IEMRException.class,
+ () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ assertEquals("Boom", thrown.getMessage());
+ }
+
+ @Test
+ void getCaseSheetFromCentralServer_failsWhenTheCentralServerRejectsTheRequest() {
+ doReturn(new ResponseEntity(HttpStatus.BAD_REQUEST)).when(serviceSpy)
+ .restTemplatePost(anyString(), any(), anyString(), any());
+
+ assertThrows(IEMRException.class,
+ () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth"));
+ }
+
+ @Test
+ void updateConfirmedDisease_delegatesToTheScreeningRepository() {
+ when(iDRSDataRepo.updateConfirmedAndSuspectedDisease("Diabetes", "Hypertension", 2L)).thenReturn(1);
+ assertEquals(1, service.updateConfirmedDisease("Diabetes", "Hypertension", 2L));
+ }
+
+ @Test
+ void getJsonObj_parsesTheResponseBody() {
+ assertTrue(service.getJsonObj(ok("{\"statusCode\":200}")).has("statusCode"));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java b/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java
new file mode 100644
index 00000000..e9ad2627
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java
@@ -0,0 +1,575 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.covid19;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus;
+import com.iemr.mmu.data.covid19.Covid19BenFeedback;
+import com.iemr.mmu.data.nurse.CommonUtilityClass;
+import com.iemr.mmu.data.quickConsultation.PrescriptionDetail;
+import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo;
+import com.iemr.mmu.repo.nurse.covid19.Covid19BenFeedbackRepo;
+import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo;
+import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl;
+import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl;
+import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl;
+import com.iemr.mmu.service.common.transaction.CommonServiceImpl;
+import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl;
+
+class Covid19ServiceImplTest {
+
+ @Mock
+ private CommonNurseServiceImpl commonNurseServiceImpl;
+ @Mock
+ private CommonDoctorServiceImpl commonDoctorServiceImpl;
+ @Mock
+ private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl;
+ @Mock
+ private LabTechnicianServiceImpl labTechnicianServiceImpl;
+ @Mock
+ private CommonServiceImpl commonServiceImpl;
+ @Mock
+ private Covid19BenFeedbackRepo covid19BenFeedbackRepo;
+ @Mock
+ private PrescriptionDetailRepo prescriptionDetailRepo;
+ @Mock
+ private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo;
+
+ @InjectMocks
+ private Covid19ServiceImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ private static JsonObject json(String raw) {
+ return JsonParser.parseString(raw).getAsJsonObject();
+ }
+
+ private static String visitDetailsBlock(String covidDetails) {
+ return "\"visitDetails\":{\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"New Chief Complaint\","
+ + "\"visitCategory\":\"COVID-19 Screening\"},\"covidDetails\":" + covidDetails + "}";
+ }
+
+ private static Covid19BenFeedback storedFeedback() {
+ Covid19BenFeedback stored = new Covid19BenFeedback();
+ stored.setcOVID19ID(BigInteger.valueOf(3));
+ return stored;
+ }
+
+ @Nested
+ @DisplayName("saving nurse data")
+ class NurseSave {
+
+ @Test
+ void saveCovid19NurseData_ignoresARequestWithoutVisitDetails() throws Exception {
+ assertNull(service.saveCovid19NurseData(null, "auth"));
+ assertNull(service.saveCovid19NurseData(json("{}"), "auth"));
+ }
+
+ @Test
+ void saveCovid19NurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception {
+ when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus());
+
+ assertEquals(0L, service.saveCovid19NurseData(json("{" + visitDetailsBlock("null") + "}"), "auth"));
+ verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any());
+ }
+
+ @Test
+ void saveCovid19NurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception {
+ when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1);
+
+ assertEquals(0L, service.saveCovid19NurseData(json("{" + visitDetailsBlock("null") + "}"), "auth"));
+ }
+
+ @Test
+ void saveCovid19NurseData_savesTheScreeningAndAdvancesTheBeneficiaryFlow() throws Exception {
+ stubVisitCreation();
+ when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback());
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1);
+
+ String request = "{" + visitDetailsBlock(covidFeedback()) + ",\"historyDetails\":{},"
+ + "\"vitalDetails\":{}}";
+
+ assertEquals(1L, service.saveCovid19NurseData(json(request), "auth"));
+ }
+
+ @Test
+ void saveCovid19NurseData_treatsAnAbsentScreeningBlockAsAlreadyDone() throws Exception {
+ stubVisitCreation();
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1);
+
+ String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}";
+
+ assertEquals(1L, service.saveCovid19NurseData(json(request), "auth"));
+ verify(covid19BenFeedbackRepo, never()).save(any());
+ }
+
+ @Test
+ void saveCovid19NurseData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception {
+ stubVisitCreation();
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(0);
+
+ String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}";
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.saveCovid19NurseData(json(request), "auth"));
+ assertTrue(thrown.getMessage().contains("Beneficiary status update failed"));
+ }
+
+ @Test
+ void saveCovid19NurseData_failsWhenASectionCouldNotBeSaved() throws Exception {
+ stubVisitCreation();
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(null);
+
+ String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}";
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.saveCovid19NurseData(json(request), "auth"));
+ assertEquals("Error occurred while saving data", thrown.getMessage());
+ }
+
+ private void stubVisitCreation() throws Exception {
+ when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0);
+ when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L);
+ when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L);
+ }
+
+ private String covidFeedback() {
+ return "{\"symptom\":[\"Fever\",\"Cough\"],\"contactStatus\":[\"Household\"],"
+ + "\"travelList\":[\"Domestic\"],\"recommendation\":[[\"Isolate\",\"Test\"]],"
+ + "\"suspectedStatusUI\":\"YES\"}";
+ }
+
+ @Test
+ void saveCovid19NurseData_joinsEveryMultiValuedScreeningAnswerBeforeSaving() throws Exception {
+ stubVisitCreation();
+ when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback());
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1);
+
+ String request = "{" + visitDetailsBlock(covidFeedback()) + ",\"historyDetails\":{},"
+ + "\"vitalDetails\":{}}";
+ service.saveCovid19NurseData(json(request), "auth");
+
+ ArgumentCaptor saved = ArgumentCaptor.forClass(Covid19BenFeedback.class);
+ verify(covid19BenFeedbackRepo).save(saved.capture());
+ assertEquals("Fever||Cough", saved.getValue().getSymptoms_db());
+ assertEquals("Household", saved.getValue().getcOVID19_contact_history());
+ assertEquals("Domestic", saved.getValue().getTravelType());
+ assertEquals("Isolate||Test", saved.getValue().getRecommendation_db());
+ assertEquals(Boolean.TRUE, saved.getValue().getSuspectedStatus());
+ }
+
+ @Test
+ void saveCovid19NurseData_recordsANegativeScreeningResult() throws Exception {
+ stubVisitCreation();
+ when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback());
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L);
+ when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(),
+ anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1);
+
+ String request = "{" + visitDetailsBlock("{\"suspectedStatusUI\":\"NO\"}")
+ + ",\"historyDetails\":{},\"vitalDetails\":{}}";
+ service.saveCovid19NurseData(json(request), "auth");
+
+ ArgumentCaptor saved = ArgumentCaptor.forClass(Covid19BenFeedback.class);
+ verify(covid19BenFeedbackRepo).save(saved.capture());
+ assertEquals(Boolean.FALSE, saved.getValue().getSuspectedStatus());
+ }
+
+ @Test
+ void saveBenVisitDetails_returnsNothingWhenTheVisitBlockIsMissing() throws Exception {
+ assertTrue(service.saveBenVisitDetails(json("{}"), new CommonUtilityClass()).isEmpty());
+ assertTrue(service.saveBenVisitDetails(null, new CommonUtilityClass()).isEmpty());
+ }
+ }
+
+ @Nested
+ @DisplayName("saving the individual nurse sections")
+ class NurseSections {
+
+ @Test
+ void saveBenCovid19HistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1L, service.saveBenCovid19HistoryDetails(json("{}"), 1L, 2L));
+ assertEquals(1L, service.saveBenCovid19HistoryDetails(null, 1L, 2L));
+ }
+
+ @Test
+ void saveBenCovid19HistoryDetails_savesEverySectionThatWasSent() throws Exception {
+ when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenComorbidConditions(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenMedicationHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveFemaleObstetricHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenMenstrualHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.saveBenFamilyHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.savePersonalHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.saveAllergyHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveChildOptionalVaccineDetail(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveImmunizationHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveChildDevelopmentHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveChildFeedingHistory(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.savePerinatalHistory(any())).thenReturn(1L);
+
+ String history = "{\"pastHistory\":{},\"comorbidConditions\":{},"
+ + "\"medicationHistory\":{\"medicationHistoryList\":[{\"currentMedication\":\"x\"}]},"
+ + "\"femaleObstetricHistory\":{},\"menstrualHistory\":{},\"familyHistory\":{},"
+ + "\"personalHistory\":{},\"childVaccineDetails\":{},\"immunizationHistory\":{},"
+ + "\"developmentHistory\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}}";
+
+ assertEquals(1L, service.saveBenCovid19HistoryDetails(json(history), 1L, 2L));
+ }
+
+ @Test
+ void saveBenCovid19HistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception {
+ when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L);
+ assertNull(service.saveBenCovid19HistoryDetails(json("{\"pastHistory\":{}}"), 1L, 2L));
+ }
+
+ @Test
+ void saveBenCovid19VitalDetails_savesAnthropometryAndVitalsTogether() throws Exception {
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L);
+
+ assertEquals(4L, service.saveBenCovid19VitalDetails(json("{}"), 1L, 2L));
+ }
+
+ @Test
+ void saveBenCovid19VitalDetails_reportsFailureWhenTheVitalsCouldNotBeSaved() throws Exception {
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L);
+ when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null);
+
+ assertNull(service.saveBenCovid19VitalDetails(json("{}"), 1L, 2L));
+ assertNull(service.saveBenCovid19VitalDetails(null, 1L, 2L));
+ }
+ }
+
+ @Nested
+ @DisplayName("reading the nurse and doctor case sheets")
+ class Reads {
+
+ @Test
+ void getBenVisitDetailsFrmNurseCovid19_splitsTheStoredScreeningAnswersBackIntoLists() throws Exception {
+ Covid19BenFeedback stored = new Covid19BenFeedback();
+ stored.setSymptoms_db("Fever||Cough");
+ stored.setTravelType("Domestic||International");
+ stored.setcOVID19_contact_history("Household");
+ stored.setRecommendation_db("Isolate||Test");
+ stored.setSuspectedStatus(true);
+ when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(stored);
+
+ String result = service.getBenVisitDetailsFrmNurseCovid19(1L, 2L);
+
+ assertTrue(result.contains("covid19NurseVisitDetail"));
+ assertEquals(2, stored.getSymptoms().length);
+ assertEquals(2, stored.getTravelList().length);
+ assertEquals(1, stored.getContactStatus().length);
+ assertEquals(1, stored.getRecommendation().size());
+ assertEquals("YES", stored.getSuspectedStatusUI());
+ }
+
+ @Test
+ void getBenVisitDetailsFrmNurseCovid19_marksANegativeScreeningForTheUi() throws Exception {
+ Covid19BenFeedback stored = new Covid19BenFeedback();
+ stored.setSuspectedStatus(false);
+ when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(stored);
+
+ service.getBenVisitDetailsFrmNurseCovid19(1L, 2L);
+
+ assertEquals("NO", stored.getSuspectedStatusUI());
+ }
+
+ @Test
+ void getBenVisitDetailsFrmNurseCovid19_tolratesABeneficiaryWithNoScreeningOnRecord() throws Exception {
+ when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(null);
+ assertTrue(service.getBenVisitDetailsFrmNurseCovid19(1L, 2L).contains("covidDetails"));
+ }
+
+ @Test
+ void getBenCovidNurseData_combinesTheScreeningVitalsAndHistory() {
+ String result = service.getBenCovidNurseData(1L, 2L);
+
+ assertTrue(result.contains("covidDetails"));
+ assertTrue(result.contains("vitals"));
+ assertTrue(result.contains("history"));
+ }
+
+ @Test
+ void getBenCovid19HistoryDetails_gathersEveryHistorySection() {
+ when(commonNurseServiceImpl.getPastHistoryData(1L, 2L))
+ .thenReturn(new com.iemr.mmu.data.anc.BenMedHistory());
+
+ assertTrue(service.getBenCovid19HistoryDetails(1L, 2L).contains("PastHistory"));
+ verify(commonNurseServiceImpl).getFeedingHistory(1L, 2L);
+ }
+
+ @Test
+ void getBeneficiaryVitalDetails_gathersAnthropometryAndVitals() {
+ when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a");
+ when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v");
+
+ assertTrue(service.getBeneficiaryVitalDetails(1L, 2L).contains("benAnthropometryDetail"));
+ }
+
+ @Test
+ void getBenCaseRecordFromDoctorCovid19_readsTheStoredDiagnosisWhenOneExists() throws Exception {
+ PrescriptionDetail prescription = new PrescriptionDetail();
+ prescription.setDiagnosisProvided("Covid-19");
+ prescription.setPrescriptionID(7L);
+ ArrayList prescriptions = new ArrayList<>(
+ Collections.singletonList(prescription));
+ when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(prescriptions);
+ when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>());
+ when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>());
+
+ String result = service.getBenCaseRecordFromDoctorCovid19(1L, 2L);
+
+ assertTrue(result.contains("Covid-19"));
+ assertTrue(result.contains("GraphData"));
+ }
+
+ @Test
+ void getBenCaseRecordFromDoctorCovid19_returnsAnEmptyDiagnosisWhenNoneWasRecorded() throws Exception {
+ when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(new ArrayList<>());
+ when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>());
+ when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>());
+
+ assertTrue(service.getBenCaseRecordFromDoctorCovid19(1L, 2L).contains("diagnosis"));
+ }
+ }
+
+ @Nested
+ @DisplayName("updating nurse data")
+ class NurseUpdates {
+
+ @Test
+ void updateBenHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception {
+ assertEquals(1, service.updateBenHistoryDetails(json("{}")));
+ }
+
+ @Test
+ void updateBenHistoryDetails_updatesEverySectionThatWasSent() throws Exception {
+ when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateBenComorbidConditions(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateBenMedicationHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateBenPersonalHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateBenAllergicHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateBenFamilyHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateMenstrualHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updatePastObstetricHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateChildImmunizationDetail(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateChildOptionalVaccineDetail(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateChildFeedingHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updatePerinatalHistory(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateChildDevelopmentHistory(any())).thenReturn(1);
+
+ String history = "{\"pastHistory\":{},\"comorbidConditions\":{},\"medicationHistory\":{},"
+ + "\"personalHistory\":{},\"familyHistory\":{},\"menstrualHistory\":{},"
+ + "\"femaleObstetricHistory\":{},\"immunizationHistory\":{\"benChildVaccineDetails\":[{}]},"
+ + "\"childVaccineDetails\":{},\"feedingHistory\":{},\"perinatalHistroy\":{},"
+ + "\"developmentHistory\":{}}";
+
+ assertEquals(1, service.updateBenHistoryDetails(json(history)));
+ }
+
+ @Test
+ void updateBenHistoryDetails_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception {
+ when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(0);
+ assertEquals(0, service.updateBenHistoryDetails(json("{\"pastHistory\":{}}")));
+ }
+
+ @Test
+ void updateBenVitalDetails_updatesAnthropometryAndVitalsTogether() throws Exception {
+ when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1);
+
+ assertEquals(1, service.updateBenVitalDetails(json("{}")));
+ }
+
+ @Test
+ void updateBenVitalDetails_reportsFailureWhenTheVitalsCouldNotBeUpdated() throws Exception {
+ when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(0);
+
+ assertEquals(0, service.updateBenVitalDetails(json("{}")));
+ assertEquals(1, service.updateBenVitalDetails(null));
+ }
+ }
+
+ @Nested
+ @DisplayName("saving and updating doctor data")
+ class DoctorData {
+
+ private String doctorRequest(boolean isSpecialist) {
+ return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4,"
+ + "\"createdBy\":\"doctor\",\"isSpecialist\":" + isSpecialist + ",\"findings\":{},"
+ + "\"investigation\":{\"laboratoryList\":[{}]},"
+ + "\"diagnosis\":{\"doctorDiagnosis\":\"Covid-19\",\"specialistDiagnosis\":\"Isolate\","
+ + "\"prescriptionID\":7},\"prescription\":[{\"drugID\":1}],\"refer\":{}}";
+ }
+
+ private Map drugResult() {
+ Map result = new HashMap<>();
+ result.put("count", 1);
+ result.put("prescribedDrugIDs", Collections.singletonList(9L));
+ return result;
+ }
+
+ @Test
+ void saveDoctorData_savesEverySectionAndAdvancesTheFlow() throws Exception {
+ when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1);
+ when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(),
+ any(), anyString())).thenReturn(7L);
+ when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult());
+ when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L);
+ when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any()))
+ .thenReturn(1);
+
+ assertEquals(1L, service.saveDoctorData(json(doctorRequest(false)), "auth"));
+ }
+
+ @Test
+ void saveDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception {
+ when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(),
+ any(), any())).thenReturn(7L);
+ when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any()))
+ .thenReturn(1);
+
+ assertEquals(1L, service.saveDoctorData(json("{\"investigation\":{}}"), "auth"));
+ }
+
+ @Test
+ void saveDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception {
+ when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1);
+ when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(),
+ any(), anyString())).thenReturn(7L);
+ when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult());
+ when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L);
+ when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any()))
+ .thenReturn(0);
+
+ assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest(false)), "auth"));
+ }
+
+ @Test
+ void saveDoctorData_failsWhenASectionCouldNotBeSaved() throws Exception {
+ when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(0);
+ when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(),
+ any(), anyString())).thenReturn(7L);
+
+ assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest(false)), "auth"));
+ }
+
+ @Test
+ void updateCovid19DoctorData_recordsTheDoctorDiagnosisForANonSpecialist() throws Exception {
+ stubSuccessfulDoctorUpdate();
+
+ assertEquals(1L, service.updateCovid19DoctorData(json(doctorRequest(false)), "auth"));
+
+ ArgumentCaptor updated = ArgumentCaptor.forClass(PrescriptionDetail.class);
+ verify(commonNurseServiceImpl).updatePrescription(updated.capture());
+ assertEquals("Covid-19", updated.getValue().getDiagnosisProvided());
+ }
+
+ @Test
+ void updateCovid19DoctorData_recordsTheSpecialistInstructionForASpecialist() throws Exception {
+ stubSuccessfulDoctorUpdate();
+
+ assertEquals(1L, service.updateCovid19DoctorData(json(doctorRequest(true)), "auth"));
+
+ ArgumentCaptor updated = ArgumentCaptor.forClass(PrescriptionDetail.class);
+ verify(commonNurseServiceImpl).updatePrescription(updated.capture());
+ assertEquals("Isolate", updated.getValue().getInstruction());
+ }
+
+ @Test
+ void updateCovid19DoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception {
+ stubSuccessfulDoctorUpdate();
+ when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any()))
+ .thenReturn(0);
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> service.updateCovid19DoctorData(json(doctorRequest(false)), "auth"));
+ assertTrue(thrown.getMessage().contains("Beneficiary status update failed"));
+ }
+
+ @Test
+ void updateCovid19DoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception {
+ when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(0);
+
+ assertThrows(RuntimeException.class,
+ () -> service.updateCovid19DoctorData(json(doctorRequest(false)), "auth"));
+ }
+
+ private void stubSuccessfulDoctorUpdate() throws Exception {
+ when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1);
+ when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1);
+ when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L);
+ when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult());
+ when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L);
+ when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any()))
+ .thenReturn(1);
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java
new file mode 100644
index 00000000..07fa285f
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java
@@ -0,0 +1,176 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.iemr.mmu.service.benid.BenIdAutoTopUpService;
+import com.iemr.mmu.service.benid.CentralAuthService;
+
+class AutoSyncServiceTest {
+
+ private AutoSyncService service;
+ private CentralAuthService centralAuth;
+ private UploadDataToServerImpl upload;
+ private DownloadDataFromServerImpl download;
+ private DataSyncRepository repo;
+ private BenIdAutoTopUpService benId;
+ private DownloadDataFromServerTransactionalImpl inventory;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ centralAuth = mock(CentralAuthService.class);
+ upload = mock(UploadDataToServerImpl.class);
+ download = mock(DownloadDataFromServerImpl.class);
+ repo = mock(DataSyncRepository.class);
+ benId = mock(BenIdAutoTopUpService.class);
+ inventory = mock(DownloadDataFromServerTransactionalImpl.class);
+ when(centralAuth.isEnabled()).thenReturn(true);
+ when(centralAuth.getVanID()).thenReturn(403);
+ when(centralAuth.getConnectedUser()).thenReturn("stoptb1");
+ when(centralAuth.getCentralSessionKey()).thenReturn("SESSION-KEY");
+ when(repo.getVanProviderServiceMapID(403)).thenReturn(1726);
+
+ service = new AutoSyncService();
+ ReflectionTestUtils.setField(service, "centralAuthService", centralAuth);
+ ReflectionTestUtils.setField(service, "uploadDataToServerImpl", upload);
+ ReflectionTestUtils.setField(service, "downloadDataFromServerImpl", download);
+ ReflectionTestUtils.setField(service, "dataSyncRepository", repo);
+ ReflectionTestUtils.setField(service, "benIdAutoTopUpService", benId);
+ ReflectionTestUtils.setField(service, "downloadDataFromServerTransactional", inventory);
+ ReflectionTestUtils.setField(service, "inventoryDownloadEnabled", true);
+ ReflectionTestUtils.setField(service, "uploadEnabled", true);
+ ReflectionTestUtils.setField(service, "masterDownloadEnabled", true);
+ ReflectionTestUtils.setField(service, "masterDownloadInventoryOnly", true);
+ }
+
+ @Test
+ void uploadUsesCentralKeyAndConnectedOperator() throws Exception {
+ when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced");
+
+ service.uploadOnce();
+
+ verify(upload).getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null);
+ verify(benId).triggerNow();
+ assertEquals("Data successfully synced", service.status().get("lastUploadResult"));
+ }
+
+ @Test
+ void uploadIsFollowedByInventoryDownload() throws Exception {
+ when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced");
+ when(inventory.downloadTransactionalData(403, "SESSION-KEY", "datasync")).thenReturn(1);
+
+ service.uploadOnce();
+
+ verify(inventory).downloadTransactionalData(403, "SESSION-KEY", "datasync");
+ assertEquals("success", service.status().get("lastInventoryDownloadResult"));
+ }
+
+ @Test
+ void inventoryDownloadFailureKeepsUploadResult() throws Exception {
+ when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced");
+ when(inventory.downloadTransactionalData(403, "SESSION-KEY", "datasync")).thenThrow(new RuntimeException("offline"));
+
+ service.uploadOnce();
+
+ assertEquals("Data successfully synced", service.status().get("lastUploadResult"));
+ assertEquals("Inventory download: offline", service.status().get("lastError"));
+ }
+
+ @Test
+ void noCentralConnectionSkipsUpload() throws Exception {
+ when(centralAuth.getCentralSessionKey()).thenReturn(null);
+
+ service.uploadOnce();
+
+ verify(upload, never()).getDataToSyncToServer(anyInt(), anyString(), anyString(), any());
+ assertEquals("No central connection - waiting for a user login", service.status().get("lastError"));
+ }
+
+ @Test
+ void masterDownloadUsesVanServiceLineAndOnlyInventoryMasters() throws Exception {
+ when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS))
+ .thenReturn("success");
+
+ service.masterDownloadOnce();
+
+ verify(download).downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS);
+ assertEquals("success", service.status().get("lastMasterDownloadResult"));
+ }
+
+ @Test
+ void masterDownloadCanFetchAllMasters() throws Exception {
+ ReflectionTestUtils.setField(service, "masterDownloadInventoryOnly", false);
+ when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, null)).thenReturn("success");
+
+ service.masterDownloadOnce();
+
+ verify(download).downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, null);
+ }
+
+ @Test
+ void manualMasterDownloadRunningIsNotCountedAsARun() throws Exception {
+ when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS))
+ .thenReturn("inProgress");
+
+ service.masterDownloadOnce();
+
+ assertEquals(0L, service.status().getLong("lastMasterDownloadAt"));
+ }
+
+ @Test
+ void statusSumsRejectedRows() throws Exception {
+ when(repo.getSyncFailures()).thenReturn(List.of(
+ Map.of("tableName", "t_patientissue", "reason", "Duplicate", "failedRows", 2L),
+ Map.of("tableName", "t_prescription", "reason", "Timeout", "failedRows", 3L)));
+
+ JSONObject status = service.status();
+
+ assertEquals(5L, status.getLong("failedRows"));
+ assertEquals(2, status.getJSONArray("failures").length());
+ }
+
+ @Test
+ void disabledLaptopDoesNothing() throws Exception {
+ when(centralAuth.isEnabled()).thenReturn(false);
+
+ service.scheduledUpload();
+ service.scheduledMasterDownload();
+
+ verify(centralAuth, never()).getCentralSessionKey();
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java
new file mode 100644
index 00000000..482b415e
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java
@@ -0,0 +1,208 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.MockedConstruction;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
+
+import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster;
+import com.iemr.mmu.data.syncActivity_syncLayer.TempVan;
+import com.iemr.mmu.repo.syncActivity_syncLayer.SyncDownloadMasterRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.TempVanRepo;
+import com.iemr.mmu.utils.CookieUtil;
+
+class DownloadDataFromServerImplTest {
+
+ @Mock
+ private SyncDownloadMasterRepo syncDownloadMasterRepo;
+ @Mock
+ private DataSyncRepository dataSyncRepository;
+ @Mock
+ private TempVanRepo tempVanRepo;
+ @Mock
+ private CookieUtil cookieUtil;
+
+ @InjectMocks
+ private DownloadDataFromServerImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "dataSyncDownloadUrl", "http://central/master");
+ ReflectionTestUtils.setField(service, "benGenUrlCentral", "http://central/benGen");
+ ReflectionTestUtils.setField(service, "benImportUrlLocal", "http://local/benImport");
+ // The download counters are static, so each test starts from a settled state.
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 0);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 0);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedCounter", 0);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedMasters", new StringBuilder());
+ }
+
+ private ArrayList oneMasterTable() {
+ SyncDownloadMaster master = new SyncDownloadMaster();
+ master.setDownloadMasterTableID(1);
+ master.setSchemaName("db_iemr");
+ master.setTableName("m_gender");
+ master.setVanColumnName("GenderID,GenderName");
+ ArrayList masters = new ArrayList<>();
+ masters.add(master);
+ return masters;
+ }
+
+ private MockedConstruction centralAnswering(String body) {
+ return mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)));
+ }
+
+ @Test
+ @DisplayName("a master download is accepted and the progress counters are reset")
+ void downloadMasterDataFromServer_acceptsTheRequestAndResetsTheProgressCounters() throws Exception {
+ when(syncDownloadMasterRepo.getDownloadTables()).thenReturn(oneMasterTable());
+
+ assertEquals(" Master download started ", service.downloadMasterDataFromServer("auth", "token", 1, 2));
+
+ // The download itself runs on its own thread pool; what the caller sees is the
+ // reset progress counters.
+ assertEquals(1, ReflectionTestUtils.getField(DownloadDataFromServerImpl.class, "totalCounter"));
+ assertEquals(0, ReflectionTestUtils.getField(DownloadDataFromServerImpl.class, "failedCounter"));
+ }
+
+ @Test
+ @DisplayName("a second download request while one is running reports that it is in progress")
+ void downloadMasterDataFromServer_reportsADownloadThatIsStillRunning() throws Exception {
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 5);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 1);
+
+ assertEquals("inProgress", service.downloadMasterDataFromServer("auth", "token", 1, 2));
+ }
+
+ @Test
+ @DisplayName("the download progress is reported as a percentage")
+ void getDownloadStatus_reportsTheProgressAsAPercentage() {
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 4);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 1);
+ ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedCounter", 2);
+
+ Map status = service.getDownloadStatus();
+
+ assertEquals(25.0, status.get("percentage"));
+ assertEquals(2, status.get("failedMasterCount"));
+ }
+
+ @Test
+ @DisplayName("the van assigned to this installation is reported")
+ void getVanDetailsForMasterDownload_reportsTheOnlyVanOfThisInstallation() throws Exception {
+ TempVan van = new TempVan();
+ van.setVanID(1);
+ when(tempVanRepo.getVanID()).thenReturn(new ArrayList<>(Collections.singletonList(van)));
+
+ assertTrue(service.getVanDetailsForMasterDownload().contains("\"vanID\":1"));
+ }
+
+ @Test
+ @DisplayName("more than one configured van is reported as a configuration error")
+ void getVanDetailsForMasterDownload_failsWhenMoreThanOneVanIsConfigured() {
+ when(tempVanRepo.getVanID()).thenReturn(new ArrayList<>(List.of(new TempVan(), new TempVan())));
+
+ Exception thrown = assertThrows(Exception.class, () -> service.getVanDetailsForMasterDownload());
+ assertTrue(thrown.getMessage().contains("more than 1 van"));
+ }
+
+ @Test
+ @DisplayName("a generated beneficiary id block is imported into the van")
+ void callCentralAPIToGenerateBenIDAndimportToLocal_importsTheGeneratedIdBlock() throws Exception {
+ String central = "{\"statusCode\":200,\"data\":[{\"beneficiaryID\":1}]}";
+ String local = "{\"statusCode\":200,\"data\":{}}";
+
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> {
+ when(mock.exchange(eq("http://central/benGen"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(central, HttpStatus.OK));
+ when(mock.exchange(eq("http://local/benImport"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(local, HttpStatus.OK));
+ })) {
+ assertEquals(2, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token"));
+ }
+ }
+
+ @Test
+ @DisplayName("a block the van could not import stops short of a completed import")
+ void callCentralAPIToGenerateBenIDAndimportToLocal_stopsShortWhenTheVanCannotImport() throws Exception {
+ String central = "{\"statusCode\":200,\"data\":[{\"beneficiaryID\":1}]}";
+
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> {
+ when(mock.exchange(eq("http://central/benGen"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(central, HttpStatus.OK));
+ when(mock.exchange(eq("http://local/benImport"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("{\"statusCode\":500}", HttpStatus.OK));
+ })) {
+ assertEquals(1, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token"));
+ }
+ }
+
+ @Test
+ @DisplayName("a central server that generates no ids leaves nothing to import")
+ void callCentralAPIToGenerateBenIDAndimportToLocal_leavesNothingToImportWhenNoIdWasGenerated() throws Exception {
+ try (MockedConstruction rest = centralAnswering("{\"statusCode\":500}")) {
+ assertEquals(0, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token"));
+ }
+ }
+
+ @Test
+ @DisplayName("a central server that cannot be reached is reported to the caller")
+ void callCentralAPIToGenerateBenIDAndimportToLocal_reportsACentralServerThatCannotBeReached() {
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenThrow(new RuntimeException("connection refused")))) {
+
+ Exception thrown = assertThrows(Exception.class, () -> service
+ .callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token"));
+ assertTrue(thrown.getMessage().contains("Error while generating"));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java
new file mode 100644
index 00000000..26840ce9
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java
@@ -0,0 +1,169 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.MockedConstruction;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
+
+import com.iemr.mmu.repo.login.MasterVanRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.IndentIssueRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.IndentOrderRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.IndentRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.ItemStockEntryRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.StockTransferRepo;
+import com.iemr.mmu.utils.CookieUtil;
+
+class DownloadDataFromServerTransactionalImplTest {
+
+ @Mock
+ private MasterVanRepo masterVanRepo;
+ @Mock
+ private IndentRepo indentRepo;
+ @Mock
+ private IndentOrderRepo indentOrderRepo;
+ @Mock
+ private IndentIssueRepo indentIssueRepo;
+ @Mock
+ private StockTransferRepo stockTransferRepo;
+ @Mock
+ private ItemStockEntryRepo itemStockEntryRepo;
+ @Mock
+ private CookieUtil cookieUtil;
+
+ @InjectMocks
+ private DownloadDataFromServerTransactionalImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "dataSyncTransactionDownloadUrl", "http://central/download");
+ ReflectionTestUtils.setField(service, "dataSyncProcessedFlagUpdate", "http://central/flag");
+ }
+
+ /** The central server's download payload plus its acknowledgement of the flag update. */
+ private MockedConstruction centralAnswering(String downloadBody) {
+ return mockConstruction(RestTemplate.class, (mock, context) -> {
+ when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(downloadBody, HttpStatus.OK));
+ when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("{\"statusCode\":200,\"data\":{}}", HttpStatus.OK));
+ });
+ }
+
+ @Test
+ @DisplayName("an empty download leaves the van's stock tables untouched")
+ void downloadTransactionalData_leavesTheStockTablesUntouchedWhenThereIsNothingToDownload() throws Exception {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(5);
+
+ try (MockedConstruction rest = centralAnswering("{\"data\":[]}")) {
+ assertEquals(1, service.downloadTransactionalData(1, "auth", "token"));
+ }
+ verify(indentRepo, never()).saveAll(any());
+ }
+
+ @Test
+ @DisplayName("a downloaded indent is stored against the van's own row when one already exists")
+ void downloadTransactionalData_reusesTheVanRowOfAnAlreadyDownloadedIndent() throws Exception {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(5);
+ when(indentRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(9L);
+ when(indentIssueRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(null);
+ when(itemStockEntryRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(null);
+
+ String payload = "{\"data\":[{\"indentID\":1,\"indentIssueID\":1,\"itemStockEntryID\":1,"
+ + "\"syncFacilityID\":5,\"vanSerialNo\":2}]}";
+
+ try (MockedConstruction rest = centralAnswering(payload)) {
+ assertEquals(1, service.downloadTransactionalData(1, "auth", "token"));
+ }
+ verify(indentRepo).saveAll(any());
+ verify(indentIssueRepo).saveAll(any());
+ verify(itemStockEntryRepo).saveAll(any());
+ }
+
+ @Test
+ @DisplayName("a van without a facility mapping cannot download anything")
+ void downloadTransactionalData_failsForAVanWithoutAFacilityMapping() {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(null);
+
+ Exception thrown = assertThrows(Exception.class, () -> service.downloadTransactionalData(1, "auth", "token"));
+ assertEquals("Facility mapping for this van is either missing/wrong...", thrown.getMessage());
+ }
+
+ @Test
+ @DisplayName("a rejected flag update still leaves the downloaded rows stored")
+ void downloadTransactionalData_storesTheRowsEvenWhenTheFlagUpdateIsRejected() throws Exception {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(5);
+
+ String payload = "{\"data\":[{\"indentID\":1,\"syncFacilityID\":5,\"vanSerialNo\":2}]}";
+
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> {
+ when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(payload, HttpStatus.OK));
+ when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("{\"statusCode\":500}", HttpStatus.OK));
+ })) {
+ assertEquals(1, service.downloadTransactionalData(1, "auth", "token"));
+ }
+ verify(indentRepo).saveAll(any());
+ }
+
+ @Test
+ @DisplayName("a flag update with no body still leaves the downloaded rows stored")
+ void downloadTransactionalData_storesTheRowsEvenWhenTheFlagUpdateHasNoBody() throws Exception {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(5);
+
+ String payload = "{\"data\":[{\"indentID\":1,\"syncFacilityID\":5,\"vanSerialNo\":2}]}";
+
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> {
+ when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(payload, HttpStatus.OK));
+ when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(HttpStatus.OK));
+ })) {
+ assertEquals(1, service.downloadTransactionalData(1, "auth", "token"));
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java
new file mode 100644
index 00000000..d92182b2
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java
@@ -0,0 +1,49 @@
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.sql.Timestamp;
+
+import org.junit.jupiter.api.Test;
+
+import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster;
+
+class DownloadMastersFullForLineTest {
+
+ private static final Timestamp COPIED_DATE = Timestamp.valueOf("2026-10-16 13:17:31");
+
+ private static SyncDownloadMaster table(String masterType) {
+ SyncDownloadMaster t = new SyncDownloadMaster();
+ t.setMasterType(masterType);
+ t.setLastDownloadDate(COPIED_DATE);
+ return t;
+ }
+
+ @Test
+ void serviceLineMastersAreDownloadedInFull() {
+ SyncDownloadMaster parkingPlace = table("P");
+
+ DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(parkingPlace);
+
+ assertNull(parkingPlace.getLastDownloadDate());
+ }
+
+ @Test
+ void vanMastersAreDownloadedInFull() {
+ SyncDownloadMaster vanServicePoint = table("v");
+
+ DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(vanServicePoint);
+
+ assertNull(vanServicePoint.getLastDownloadDate());
+ }
+
+ @Test
+ void mastersSharedByAllLinesStayIncremental() {
+ SyncDownloadMaster villages = table("A");
+
+ DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(villages);
+
+ assertEquals(COPIED_DATE, villages.getLastDownloadDate());
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java
new file mode 100644
index 00000000..9764af64
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java
@@ -0,0 +1,405 @@
+/*
+* AMRIT – Accessible Medical Records via Integrated Technology
+* Integrated EHR (Electronic Health Records) Solution
+*
+* Copyright (C) "Piramal Swasthya Management and Research Institute"
+*
+* This file is part of AMRIT.
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.MockedConstruction;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestTemplate;
+
+import com.iemr.mmu.data.syncActivity_syncLayer.DataSyncGroups;
+import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass;
+import com.iemr.mmu.repo.syncActivity_syncLayer.DataSyncGroupsRepo;
+import com.iemr.mmu.repo.syncActivity_syncLayer.SyncUtilityClassRepo;
+import com.iemr.mmu.repo.login.MasterVanRepo;
+import com.iemr.mmu.utils.CookieUtil;
+
+class UploadDataToServerImplTest {
+
+ @Mock
+ private DataSyncRepository dataSyncRepository;
+ @Mock
+ private DataSyncGroupsRepo dataSyncGroupsRepo;
+ @Mock
+ private MasterVanRepo masterVanRepo;
+ @Mock
+ private SyncUtilityClassRepo syncutilityClassRepo;
+ @Mock
+ private CookieUtil cookieUtil;
+
+ @InjectMocks
+ private UploadDataToServerImpl service;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ ReflectionTestUtils.setField(service, "dataSyncUploadUrl", "http://central/sync");
+ ReflectionTestUtils.setField(service, "BATCH_SIZE", 2);
+ }
+
+ private DataSyncGroups group() {
+ DataSyncGroups group = new DataSyncGroups();
+ group.setSyncTableGroupID(1);
+ group.setSyncTableGroupName("Beneficiary");
+ return group;
+ }
+
+ private SyncUtilityClass table(String tableName) {
+ SyncUtilityClass table = new SyncUtilityClass();
+ table.setSchemaName("db_iemr");
+ table.setTableName(tableName);
+ table.setVanColumnName("VanSerialNo,BeneficiaryRegID");
+ table.setServerColumnName("VanSerialNo,BeneficiaryRegID");
+ table.setVanAutoIncColumnName("VanSerialNo");
+ return table;
+ }
+
+ private List> rows(int count) {
+ List> rows = new ArrayList<>();
+ for (int i = 1; i <= count; i++) {
+ Map row = new HashMap<>();
+ row.put("VanSerialNo", i);
+ rows.add(row);
+ }
+ return rows;
+ }
+
+ /** The central server's per-record acknowledgement for a batch of van serial numbers. */
+ private String recordsResponse(boolean... outcomes) {
+ StringBuilder records = new StringBuilder();
+ for (int i = 0; i < outcomes.length; i++) {
+ if (i > 0) {
+ records.append(",");
+ }
+ records.append("{\"vanSerialNo\":\"").append(i + 1).append("\",\"success\":").append(outcomes[i])
+ .append(outcomes[i] ? "" : ",\"reason\":\"duplicate row\"").append("}");
+ }
+ return "{\"statusCode\":200,\"errorMessage\":\"Success\",\"data\":{\"records\":[" + records + "]}}";
+ }
+
+ /** A server that acknowledges every row of whatever batch it is sent. */
+ private MockedConstruction serverAcknowledgingEveryRow() {
+ return mockConstruction(RestTemplate.class, (mock, context) -> when(
+ mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))).thenAnswer(invocation -> {
+ org.springframework.http.HttpEntity> request = invocation.getArgument(2);
+ com.google.gson.JsonArray syncData = com.google.gson.JsonParser
+ .parseString(String.valueOf(request.getBody())).getAsJsonObject()
+ .getAsJsonArray("syncData");
+ boolean[] outcomes = new boolean[syncData.size()];
+ java.util.Arrays.fill(outcomes, true);
+ return new ResponseEntity<>(recordsResponse(outcomes), HttpStatus.OK);
+ }));
+ }
+
+ private MockedConstruction serverAnswering(String body) {
+ return mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)));
+ }
+
+ @Nested
+ @DisplayName("uploading a van's data")
+ class Upload {
+
+ @Test
+ void getDataToSyncToServer_reportsThatThereWasNothingToSync() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(new ArrayList<>());
+
+ assertEquals("No data to sync", service.getDataToSyncToServer(1, "nurse", "auth", "token"));
+ }
+
+ @Test
+ void getDataToSyncToServer_reportsACompletelySuccessfulSync() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(rows(2));
+
+ try (MockedConstruction rest = serverAnswering(recordsResponse(true, true))) {
+ String response = service.getDataToSyncToServer(1, "nurse", "auth", "token");
+
+ assertTrue(response.contains("Data sync completed successfully"));
+ assertTrue(response.contains("\"status\" : \"completed\""));
+ }
+ verify(dataSyncRepository).updateProcessedFlagInVan(eq("db_iemr"), eq("i_beneficiary"), any(),
+ eq("VanSerialNo"), eq("nurse"), eq("P"), eq("null"));
+ }
+
+ @Test
+ void getDataToSyncToServer_reportsAPartiallySuccessfulTable() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(rows(2));
+
+ try (MockedConstruction rest = serverAnswering(recordsResponse(true, false))) {
+ String response = service.getDataToSyncToServer(1, "nurse", "auth", "token");
+
+ assertTrue(response.contains("\"status\" : \"partial\""), response);
+ assertTrue(response.contains("\"failedRecords\" : 1"), response);
+ }
+ verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(),
+ anyString(), eq("F"), eq("duplicate row"));
+ }
+
+ @Test
+ void getDataToSyncToServer_reportsATableThatFailedEntirely() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(rows(2));
+
+ try (MockedConstruction rest = serverAnswering(recordsResponse(false, false))) {
+ String response = service.getDataToSyncToServer(1, "nurse", "auth", "token");
+
+ assertTrue(response.contains("Data sync completed with failures"), response);
+ assertTrue(response.contains("\"status\" : \"failed\""), response);
+ }
+ }
+
+ @Test
+ void getDataToSyncToServer_splitsALargeTableIntoBatchesAndARemainder() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(rows(3));
+
+ try (MockedConstruction rest = serverAcknowledgingEveryRow()) {
+ String response = service.getDataToSyncToServer(1, "nurse", "auth", "token");
+
+ assertTrue(response.contains("Data sync completed successfully"), response);
+ assertTrue(response.contains("\"totalRecords\" : 3"), response);
+ }
+ }
+
+ @Test
+ void getDataToSyncToServer_stopsTheTableWhenTheServerReportsAnError() throws Exception {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(List.of(table("i_beneficiary")));
+ when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString()))
+ .thenReturn(rows(2));
+
+ String error = "{\"statusCode\":500,\"errorMessage\":\"Central server unavailable\"}";
+ try (MockedConstruction rest = serverAnswering(error)) {
+ String response = service.getDataToSyncToServer(1, "nurse", "auth", "token");
+
+ assertTrue(response.contains("Data sync completed with failures"), response);
+ assertTrue(response.contains("\"status\" : \"failed\""), response);
+ }
+ verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(),
+ anyString(), eq("F"), eq("Central server unavailable"));
+ }
+ }
+
+ @Nested
+ @DisplayName("sending one batch to the central server")
+ class SendBatch {
+
+ @Test
+ void syncDataToServer_reportsSuccessForEveryAcknowledgedRecord() throws Exception {
+ when(masterVanRepo.getFacilityID(1)).thenReturn(5);
+
+ try (MockedConstruction rest = serverAnswering(recordsResponse(true, true))) {
+ Map result = service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo",
+ "VanSerialNo", rows(2), "nurse", "auth", "token");
+
+ assertEquals("Data successfully synced", result.get("status"));
+ assertEquals(2, result.get("successCount"));
+ assertEquals(0, result.get("failCount"));
+ }
+ }
+
+ @Test
+ void syncDataToServer_reportsAPartialSuccess() throws Exception {
+ try (MockedConstruction rest = serverAnswering(recordsResponse(true, false))) {
+ assertEquals("Partial success", service.syncDataToServer(1, "db_iemr", "i_beneficiary",
+ "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token").get("status"));
+ }
+ }
+
+ @Test
+ void syncDataToServer_acceptsTheBeneficiaryIdMappingAcknowledgement() throws Exception {
+ String body = "{\"statusCode\":200,\"errorMessage\":\"Success\","
+ + "\"data\":{\"response\":\"Data sync success\"}}";
+
+ try (MockedConstruction rest = serverAnswering(body)) {
+ Map result = service.syncDataToServer(1, "db_iemr", "m_beneficiaryregidmapping",
+ "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token");
+
+ assertEquals("Data successfully synced", result.get("status"));
+ assertEquals(2, result.get("successCount"));
+ }
+ }
+
+ @Test
+ void syncDataToServer_reportsAFailedBeneficiaryIdMapping() throws Exception {
+ String body = "{\"statusCode\":200,\"errorMessage\":\"Success\",\"data\":{\"response\":\"rejected\"}}";
+
+ try (MockedConstruction rest = serverAnswering(body)) {
+ Map result = service.syncDataToServer(1, "db_iemr", "m_beneficiaryregidmapping",
+ "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token");
+
+ assertEquals("Sync failed", result.get("status"));
+ assertEquals(2, result.get("failCount"));
+ }
+ }
+
+ @Test
+ void syncDataToServer_reportsAnUnparseableServerResponse() throws Exception {
+ try (MockedConstruction rest = serverAnswering("not json")) {
+ Map result = service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo",
+ "VanSerialNo", rows(2), "nurse", "auth", "token");
+
+ assertEquals("Sync failed", result.get("status"));
+ }
+ verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(),
+ anyString(), eq("F"), eq("Invalid server response"));
+ }
+
+ @Test
+ void syncDataToServer_reportsAnEmptyServerResponse() throws Exception {
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenReturn(new ResponseEntity<>(HttpStatus.OK)))) {
+
+ assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo",
+ "VanSerialNo", rows(2), "nurse", "auth", "token").get("status"));
+ }
+ verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(),
+ anyString(), eq("F"), eq("Empty server response"));
+ }
+
+ @Test
+ void syncDataToServer_reportsAServerThatCannotBeReached() throws Exception {
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenThrow(new ResourceAccessException("connection refused")))) {
+
+ assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo",
+ "VanSerialNo", rows(2), "nurse", "auth", "token").get("status"));
+ }
+ }
+
+ @Test
+ void syncDataToServer_reportsAnUnexpectedFailure() throws Exception {
+ try (MockedConstruction rest = mockConstruction(RestTemplate.class,
+ (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class)))
+ .thenThrow(new IllegalStateException("boom")))) {
+
+ assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo",
+ "VanSerialNo", rows(2), "nurse", "auth", "token").get("status"));
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("sync helpers")
+ class Helpers {
+
+ @Test
+ void getVanAndServerColumnList_readsTheTablesOfAGroup() throws Exception {
+ List tables = List.of(table("i_beneficiary"));
+ when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false))
+ .thenReturn(tables);
+
+ assertEquals(tables, service.getVanAndServerColumnList(1));
+ }
+
+ @Test
+ void getVanSerialNoListForSyncedData_joinsTheVanSerialNumbersWithCommas() throws Exception {
+ assertEquals("1,2,3", service.getVanSerialNoListForSyncedData("VanSerialNo", rows(3)).toString());
+ assertEquals("", service.getVanSerialNoListForSyncedData("VanSerialNo", new ArrayList<>()).toString());
+ }
+
+ @Test
+ void getDataSyncGroupDetails_serialisesTheConfiguredGroups() {
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group())));
+ assertTrue(service.getDataSyncGroupDetails().contains("Beneficiary"));
+
+ when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(null);
+ assertNull(service.getDataSyncGroupDetails());
+ }
+
+ @Test
+ void syncResult_carriesTheOutcomeOfOneTable() {
+ SyncResult result = new SyncResult("db_iemr", "i_beneficiary", "1", "nurse", false, "duplicate row");
+
+ assertEquals("db_iemr", result.getSchemaName());
+ assertEquals("i_beneficiary", result.getTableName());
+ assertEquals("1", result.getVanSerialNo());
+ assertEquals("nurse", result.getSyncedBy());
+ assertEquals(false, result.isSuccess());
+ assertEquals("duplicate row", result.getReason());
+
+ result.setSuccess(true);
+ assertEquals(true, result.isSuccess());
+
+ SyncResult same = new SyncResult("db_iemr", "i_beneficiary", "1", "nurse", true, "duplicate row");
+ assertEquals(same, result);
+ assertEquals(same.hashCode(), result.hashCode());
+ assertTrue(result.toString().contains("i_beneficiary"));
+
+ result.setSchemaName("db_identity");
+ result.setTableName("m_beneficiaryregidmapping");
+ result.setVanSerialNo("2");
+ result.setSyncedBy("doctor");
+ result.setReason(null);
+ org.junit.jupiter.api.Assertions.assertNotEquals(same, result);
+ }
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java
new file mode 100644
index 00000000..adbc39de
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java
@@ -0,0 +1,48 @@
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+class UploadRenamedColumnTest {
+
+ private static final String VAN = "status,error_message,reason_for_refusal,push_response_json,"
+ + "date_format(last_mod_date,'%Y-%m-%d %H:%i:%s'),vanSerialNo";
+ private static final String SERVER = "status,error_message,reason_for_refusal,push_response_json,last_mod_date,vanSerialNo";
+
+ @Test
+ void oldNameInSyncListUsesTheTableName() {
+ Set table = Set.of("status", "error_message", "reason_to_close", "push_response_json", "last_mod_date",
+ "vanserialno");
+
+ String[] cols = UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", VAN, SERVER, table);
+
+ assertArrayEquals(new String[] { VAN.replace("reason_for_refusal", "reason_to_close"),
+ SERVER.replace("reason_for_refusal", "reason_to_close") }, cols);
+ }
+
+ @Test
+ void tableStillWithOldNameIsUnchanged() {
+ Set table = Set.of("status", "error_message", "reason_for_refusal", "push_response_json");
+
+ assertArrayEquals(new String[] { VAN, SERVER },
+ UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", VAN, SERVER, table));
+ }
+
+ @Test
+ void newNameInSyncListOnOlderTable() {
+ String van = "status,reason_to_close", server = "status,reason_to_close";
+
+ assertArrayEquals(new String[] { "status,reason_for_refusal", "status,reason_for_refusal" },
+ UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", van, server,
+ Set.of("status", "reason_for_refusal")));
+ }
+
+ @Test
+ void otherTablesAreUnchanged() {
+ assertArrayEquals(new String[] { VAN, SERVER }, UploadDataToServerImpl.useRenamedColumns("t_tbsuspected", VAN,
+ SERVER, Set.of("reason_to_close")));
+ }
+}
diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java
new file mode 100644
index 00000000..438922a5
--- /dev/null
+++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java
@@ -0,0 +1,51 @@
+package com.iemr.mmu.service.dataSyncActivity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass;
+
+class UploadSkipDuplicateGroupTest {
+
+ private static SyncUtilityClass table(int group, String name) {
+ SyncUtilityClass t = new SyncUtilityClass();
+ t.setSyncTableGroupID(group);
+ t.setTableName(name);
+ return t;
+ }
+
+ private static List names(List tables) {
+ return tables.stream().map(SyncUtilityClass::getTableName).collect(Collectors.toList());
+ }
+
+ // as on the laptops: inventory tables in group 5 (old lists) and group 9 (complete)
+ private final List