diff --git a/pom.xml b/pom.xml
index f577db19..7883df9a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -292,6 +292,12 @@
h2
runtime
+
+
+ org.apache.commons
+ commons-csv
+ 1.11.0
+
org.apache.commons
diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties
index faf5180f..34875d64 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
diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties
index 28f6b89e..be5f3608 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}
+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}
+diagnostic.documents.storage-root=${DIAGNOSTIC_DOCUMENTS_STORAGE_ROOT}
+
## TC specialist slot booking, # not required
tcSpecialistSlotBook=${TM_API}schedule/bookSlot
diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties
index 294c0e45..515bb23c 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
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..af02dcbb
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/config/S3ClientConfig.java
@@ -0,0 +1,42 @@
+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.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;
+
+ private StaticCredentialsProvider credentialsProvider() {
+ 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/dataSyncActivity/StartSyncActivity.java b/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java
index a7f2a7c7..0e9367d6 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,6 +33,7 @@
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;
@@ -38,6 +41,8 @@
import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerImpl;
import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerTransactionalImpl;
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 +65,12 @@ public class StartSyncActivity {
@Autowired
private DownloadDataFromServerTransactionalImpl downloadDataFromServerTransactionalImpl;
@Autowired
+ private DiagnosticDocumentPushServiceImpl diagnosticDocumentPushServiceImpl;
+ @Autowired
+ private DiagnosticDocumentFetchService diagnosticDocumentFetchService;
+ @Autowired
private DownSyncDataFromServerImpl downSyncDataFromServerImpl;
+
private static final String GROUP_ID = "groupID";
private static final String PROVIDER_SERVICE_MAP_ID = "providerServiceMapID";
@@ -91,6 +101,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, villageId);
+ 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() {
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/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..b8e596b5
--- /dev/null
+++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java
@@ -0,0 +1,117 @@
+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();
+
+ @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