From a47b709730b9134a39fc2d99b425b832473f2b20 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 4 Aug 2026 13:33:25 +0530 Subject: [PATCH 1/5] fix: call report issue for 104 --- .../environment/common_example.properties | 7 +- .../NHMDetailCallReportScheduler.java | 19 ++- .../nhm_dashboard/DetailedCallReportRepo.java | 12 ++ .../ctiCall/CallCentreDataSyncImpl.java | 124 ++++++++++-------- .../NHM_DashboardServiceImpl.java | 86 +++++++++--- 5 files changed, 166 insertions(+), 82 deletions(-) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index ac440991..8b23d6f5 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -71,9 +71,9 @@ cron-scheduler-ctidatasync=0 30 01 * * ? * ##-------------------------------###cti data check with call detail report Scheduler------------------------------------------------------ -#Runs at everyday 12:10AM +#Runs at everyday 3:00AM - after the NHM data pull and CTI data sync complete start-ctidatacheck-scheduler=false -cron-scheduler-ctidatacheck=0 00 02 * * * +cron-scheduler-ctidatacheck=0 00 03 * * * ##---------------------------------#### Registration schedular for Avni------------------------------------------------------------------------------ @@ -93,7 +93,8 @@ cron-scheduler-everwelldatasync=0 0/5 * * * ? * ##-----------------------------------------------#NHM data dashboard schedular---------------------------------------------------------------- # run at everyday 12:01AM start-nhmdashboard-scheduler=true -cron-scheduler-nhmdashboard=0 1 * * * ? * +cron-scheduler-nhmdashboard=0 1 0 * * ? * +nhm-detailedcallreport-backfill-days=7 ##----------------------------------------------------#grievance data sync----------------------------------------------------------- start-grievancedatasync-scheduler=false diff --git a/src/main/java/com/iemr/common/controller/nhmdashboard/NHMDetailCallReportScheduler.java b/src/main/java/com/iemr/common/controller/nhmdashboard/NHMDetailCallReportScheduler.java index d02eb3f2..31e72019 100644 --- a/src/main/java/com/iemr/common/controller/nhmdashboard/NHMDetailCallReportScheduler.java +++ b/src/main/java/com/iemr/common/controller/nhmdashboard/NHMDetailCallReportScheduler.java @@ -41,18 +41,23 @@ public class NHMDetailCallReportScheduler { @Value("${start-ctidatacheck-scheduler}") private boolean startCtiDataCheckFlag; + /** + * Number of days (ending yesterday) checked against t_bencall. Kept in sync with + * the detailed call report backfill window, so that days pulled late from CTI are + * also reconciled. The reconciliation itself is idempotent. + */ + @Value("${nhm-detailedcallreport-backfill-days:7}") + private int lookBackDays; @Scheduled(cron = "${cron-scheduler-ctidatacheck}") public void detailedCallReport() { if (startCtiDataCheckFlag) { try { - String endDate = null; - String fromDate = null; - LocalDateTime date = null; - date = LocalDateTime.now().minusDays(1); - String[] dateArr = date.toString().split("T"); - endDate = dateArr[0].concat(" 23:59:59"); - fromDate = dateArr[0].concat(" 00:00:01"); + int days = lookBackDays > 0 ? lookBackDays : 1; + LocalDateTime endDay = LocalDateTime.now().minusDays(1); + LocalDateTime startDay = endDay.minusDays(days - 1L); + String endDate = endDay.toString().split("T")[0].concat(" 23:59:59"); + String fromDate = startDay.toString().split("T")[0].concat(" 00:00:00"); Timestamp fromTime = Timestamp.valueOf(fromDate); Timestamp endTime = Timestamp.valueOf(endDate); diff --git a/src/main/java/com/iemr/common/repository/nhm_dashboard/DetailedCallReportRepo.java b/src/main/java/com/iemr/common/repository/nhm_dashboard/DetailedCallReportRepo.java index bb89881b..f4c5b125 100644 --- a/src/main/java/com/iemr/common/repository/nhm_dashboard/DetailedCallReportRepo.java +++ b/src/main/java/com/iemr/common/repository/nhm_dashboard/DetailedCallReportRepo.java @@ -21,10 +21,13 @@ */ package com.iemr.common.repository.nhm_dashboard; +import java.sql.Date; import java.sql.Timestamp; import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.iemr.common.data.nhm_dashboard.DetailedCallReport; @@ -32,4 +35,13 @@ @Repository public interface DetailedCallReportRepo extends CrudRepository { List findByCallStartTimeBetween(Timestamp startDate, Timestamp endDate); + + /** + * Call dates for which data has already been pulled from CTI. Used to detect + * the days that were missed by earlier scheduler runs, so that they can be + * pulled again instead of staying permanently empty. + */ + @Query(value = "select distinct date(Call_Start_Time) from t_DetailedCallReport " + + "where Call_Start_Time between :startDate and :endDate", nativeQuery = true) + List findExistingCallDates(@Param("startDate") Timestamp startDate, @Param("endDate") Timestamp endDate); } diff --git a/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java b/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java index a729eaf8..d889c3fe 100644 --- a/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java +++ b/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java @@ -81,10 +81,10 @@ public String callUrl(String urlRequest) { @Override public void ctiDataSync() { LocalDate currentDate = LocalDate.now(); - // Calculate three days before the current date - LocalDate startDate = currentDate.minusDays(3); - // Calculate two days before the current date - LocalDate endDate = currentDate.minusDays(2); + // Look back 7 days to retry records that failed in previous runs + LocalDate startDate = currentDate.minusDays(7); + // Up to yesterday + LocalDate endDate = currentDate.minusDays(1); // Convert LocalDate to LocalDateTime to set time as 00:00:00 LocalDateTime startDateTime = startDate.atTime(0, 0, 0); LocalDateTime endDateTime = endDate.atTime(23, 59, 59); @@ -96,64 +96,78 @@ public void ctiDataSync() { List list = callReportRepo.getAllBenCallIDetails(startTimeStamp, endTimeStamp); if (!list.isEmpty()) { - - // List benList = new ArrayList<>(); - String callDuartion = null; - String filePath = null; - String URL = null; - String callinfoapiURL = null; - String ctiResponse = null; - String callEndTime = null; - String callStartTime = null; - String recordingPath = ""; + logger.info("Total records to process for CTI data sync: " + list.size()); for (BeneficiaryCall call : list) { - if (call.getCallID() != null) { - recordingPath = null; - try { - JSONObject requestFile = new JSONObject(); - requestFile.put("agent_id", call.getAgentID()); - requestFile.put("session_id", call.getCallID()); - - OutputResponse response1 = ctiService.getVoiceFileNew(requestFile.toString(), "extra parameter"); - if(response1 != null && response1.getStatusCode() == 200) { - - CTIResponse ctiResponsePath = InputMapper.gson().fromJson(response1.getData(), - CTIResponse.class); - String recordingFilePath = ctiResponsePath.getResponse().toString(); - if(recordingFilePath.length() > 20) - recordingPath = recordingFilePath.substring(20); - logger.info("recordingPath: " + recordingPath); - } - - callDuartion = null; - callinfoapiURL = this.callinfoapiURL; - URL = callinfoapiURL.replace("CTI_SERVER", ctiServerIP).replace("AGENT_ID", call.getAgentID()) - .replace("SESSION_ID", call.getCallID()).replace("PHONE_NO", call.getPhoneNo()); - - logger.info("calling CTI API url: " + URL); - ctiResponse = this.callUrl(URL); - logger.info("calling CTI_CDR_CALL_INFO API returned " + ctiResponse); - - CTIData data = InputMapper.gson().fromJson(ctiResponse, CTIData.class); - CTIResponse model = data.getResponse(); - - if (model.getResponse_code().equals("1")) { - callDuartion = model.getCall_duration(); - callEndTime = model.getCall_end_date_time(); - callStartTime = model.getCall_start_date_time(); - } - if (callDuartion != null) - call.setCZcallDuration(Integer.parseInt(callDuartion)); - call.setRecordingPath(recordingPath); + if (call.getCallID() == null) { + logger.warn("Skipping record with null callID, benCallID: " + call.getBenCallID()); + continue; + } + String recordingPath = null; + String callDuartion = null; + String callEndTime = null; + String callStartTime = null; + try { + JSONObject requestFile = new JSONObject(); + requestFile.put("agent_id", call.getAgentID()); + requestFile.put("session_id", call.getCallID()); + + OutputResponse response1 = ctiService.getVoiceFileNew(requestFile.toString(), "extra parameter"); + if(response1 != null && response1.getStatusCode() == 200) { + + CTIResponse ctiResponsePath = InputMapper.gson().fromJson(response1.getData(), + CTIResponse.class); + String recordingFilePath = ctiResponsePath.getResponse().toString(); + if(recordingFilePath.length() > 20) + recordingPath = recordingFilePath.substring(20); + else if (!recordingFilePath.isEmpty()) + recordingPath = recordingFilePath; + logger.info("recordingPath: " + recordingPath); + } + + String callInfoURL = this.callinfoapiURL; + String URL = callInfoURL.replace("CTI_SERVER", ctiServerIP).replace("AGENT_ID", call.getAgentID()) + .replace("SESSION_ID", call.getCallID()).replace("PHONE_NO", call.getPhoneNo()); + + logger.info("calling CTI API url: " + URL); + String ctiResponse = this.callUrl(URL); + logger.info("calling CTI_CDR_CALL_INFO API returned " + ctiResponse); + + CTIData data = InputMapper.gson().fromJson(ctiResponse, CTIData.class); + CTIResponse model = data.getResponse(); + + if (model != null && "1".equals(model.getResponse_code())) { + callDuartion = model.getCall_duration(); + callEndTime = model.getCall_end_date_time(); + callStartTime = model.getCall_start_date_time(); + } else { + logger.warn("CTI API returned non-success for sessionID: " + call.getCallID() + + ", response_code: " + (model != null ? model.getResponse_code() : "null")); + } + + // Only save if we got at least the call duration from CTI + if (callDuartion != null) { + call.setCZcallDuration(Integer.parseInt(callDuartion)); call.setCZcallEndTime(callEndTime); call.setCZcallStartTime(callStartTime); + call.setRecordingPath(recordingPath); + callReportRepo.save(call); + logger.info("CTI data sync saved for benCallID: " + call.getBenCallID()); + } else if (recordingPath != null) { + // Duration not available yet, but recording path is — save path only + call.setRecordingPath(recordingPath); callReportRepo.save(call); - logger.info("calling CTI_CDR_CALL_INFO after API call save response " + call); - } catch (Exception e) { - logger.error("VoiceFile failed with error " + e.getMessage(), e); + logger.info("Only recordingPath saved (duration pending) for benCallID: " + call.getBenCallID()); + } else { + logger.warn("No CTI data available yet for sessionID: " + call.getCallID() + + ", benCallID: " + call.getBenCallID() + " - will retry next run"); } + } catch (Exception e) { + logger.error("CTI data sync failed for benCallID: " + call.getBenCallID() + + ", sessionID: " + call.getCallID() + " - " + e.getMessage(), e); } } + } else { + logger.info("No pending records found for CTI data sync"); } } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java index d7afd579..3360cdd7 100644 --- a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java +++ b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java @@ -24,10 +24,13 @@ import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +68,13 @@ public class NHM_DashboardServiceImpl implements NHM_DashboardService { @Value("${cti-server-ip}") private String serverURL; + /** + * Number of days (ending yesterday) the detailed call report pull looks back to + * re-pull days that were missed. 1 = previous day only, i.e. old behaviour. + */ + @Value("${nhm-detailedcallreport-backfill-days:7}") + private int detailedCallReportBackfillDays; + public String pushAbandonCalls(AbandonCallSummary abandonCallSummary) throws Exception { logger.info("NHM_abandon call push API request : " + abandonCallSummary.toString()); @@ -134,17 +144,59 @@ public String pull_NHM_Data_CTI() throws IEMRException { logger.error(e.getLocalizedMessage()); } - try { - List detailedCallReportList = callDetailedCallReportCTI_API(); - if (detailedCallReportList.size() > 0) { - result2 = saveDetailedCallReport(detailedCallReportList); + StringBuilder detailedCallReportResult = new StringBuilder(); + // each pending day is pulled separately, so that one failing day does not stop + // the remaining days + for (LocalDate callDate : getPendingDetailedCallReportDates()) { + try { + List detailedCallReportList = callDetailedCallReportCTI_API(callDate); + if (detailedCallReportList.size() > 0) { + detailedCallReportResult.append(callDate).append(" : ") + .append(saveDetailedCallReport(detailedCallReportList)).append("; "); + } + } catch (Exception e) { + logger.error("DetailedCallReport pull failed for " + callDate + " - " + e.getLocalizedMessage()); + } + } + result2 = detailedCallReportResult.toString(); + + return response.concat(result1).concat(" ").concat(result2); + } + /** + * Days (oldest first) for which detailed call report data still has to be + * pulled from CTI - yesterday plus any earlier day within the backfill window + * that has no data at all. Without this, a day missed because CTI was down or + * throttled ("Please wait for 1 hour") was never requested again and stayed + * permanently missing from the report. + */ + List getPendingDetailedCallReportDates() { + LocalDate lastDate = LocalDate.now().minusDays(1); + int lookBackDays = detailedCallReportBackfillDays > 0 ? detailedCallReportBackfillDays : 1; + LocalDate firstDate = lastDate.minusDays(lookBackDays - 1L); + + Set existingDates = new HashSet<>(); + try { + List dates = detailedCallReportRepo.findExistingCallDates( + Timestamp.valueOf(firstDate.atStartOfDay()), + Timestamp.valueOf(lastDate.atTime(LocalTime.MAX).withNano(0))); + for (java.sql.Date date : dates) { + if (date != null) + existingDates.add(date.toLocalDate()); } } catch (Exception e) { - logger.error(e.getLocalizedMessage()); + // on any problem in gap detection, fall back to the previous behaviour + logger.error("Error while detecting missing detailed call report dates - " + e.getLocalizedMessage()); + return Arrays.asList(lastDate); } - return response.concat(result1).concat(" ").concat(result2); + List pendingDates = new ArrayList<>(); + for (LocalDate date = firstDate; !date.isAfter(lastDate); date = date.plusDays(1)) { + if (!existingDates.contains(date)) + pendingDates.add(date); + } + logger.info("DetailedCallReport pending dates between " + firstDate + " and " + lastDate + " : " + pendingDates); + return pendingDates; } public String saveAgentSummaryReport(List agentSummaryReportList) throws IEMRException { @@ -213,8 +265,8 @@ public List callAgentSummaryReportCTI_API() throws IEMRExcep date = LocalDateTime.now().minusDays(1); String[] dateArr = date.toString().split("T"); endDate = dateArr[0].concat(" 23:59:59"); - fromDate = dateArr[0].concat(" 00:00:01"); - + fromDate = dateArr[0].concat(" 00:00:00"); + // if (job != null && job.toLowerCase().contains("hour")) { // String jobVal = job.split(" ")[0]; // LocalDateTime nowTime = LocalDateTime.now(); @@ -248,18 +300,18 @@ else if (response.toLowerCase().contains("no data")) } public List callDetailedCallReportCTI_API() throws IEMRException { + return callDetailedCallReportCTI_API(LocalDate.now().minusDays(1)); + } + + public List callDetailedCallReportCTI_API(LocalDate callDate) throws IEMRException { List detailedCallReportList = new ArrayList(); // String job = ConfigProperties.getPropertyByName("get-details-call-report-job"); - String endDate = null; - String fromDate = null; - - LocalDateTime date = null; - date = LocalDateTime.now().minusDays(1); - String[] dateArr = date.toString().split("T"); - endDate = dateArr[0].concat(" 23:59:59"); - fromDate = dateArr[0].concat(" 00:00:01"); - + // full day window - 00:00:00 and not 00:00:01, else calls placed in the very + // first second of the day are dropped + String fromDate = callDate.toString().concat(" 00:00:00"); + String endDate = callDate.toString().concat(" 23:59:59"); + // if (job != null && job.toLowerCase().contains("hour")) { // String jobVal = job.split(" ")[0]; // LocalDateTime nowTime = LocalDateTime.now(); From 351c52442baffc3601f354f6d7a8e2229d04bafc Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Mon, 27 Jul 2026 12:13:10 +0530 Subject: [PATCH 2/5] fix: update cti logger properties --- .../com/iemr/common/data/callhandling/BeneficiaryCall.java | 7 ++++++- .../common/service/ctiCall/CallCentreDataSyncImpl.java | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/data/callhandling/BeneficiaryCall.java b/src/main/java/com/iemr/common/data/callhandling/BeneficiaryCall.java index c2c1ed10..1eca8fec 100644 --- a/src/main/java/com/iemr/common/data/callhandling/BeneficiaryCall.java +++ b/src/main/java/com/iemr/common/data/callhandling/BeneficiaryCall.java @@ -23,6 +23,8 @@ import java.sql.Timestamp; +import org.springframework.beans.factory.annotation.Value; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.gson.annotations.Expose; import com.iemr.common.data.beneficiary.Beneficiary; @@ -234,6 +236,9 @@ public class BeneficiaryCall { @Column(name = "InsName") private String instName; + @Value("${cti-logger_base_url}") + private String loggerBaseURL; + @Transient @Expose private String[] instNames; @@ -280,7 +285,7 @@ public BeneficiaryCall(Long beneficiaryRegID, Boolean is1097, String createdBy) public BeneficiaryCall(Long benCallID, Timestamp createdDate, String agentID, String callID, String recordingPath, String archivePath) { - String loggerBaseURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); + // String loggerBaseURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); this.benCallID = benCallID; this.createdDate = createdDate; this.agentID = agentID; diff --git a/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java b/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java index d889c3fe..f568fe9e 100644 --- a/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java +++ b/src/main/java/com/iemr/common/service/ctiCall/CallCentreDataSyncImpl.java @@ -63,7 +63,9 @@ public class CallCentreDataSyncImpl implements CallCentreDataSync { private static HttpUtils httpUtils; @Autowired private CTIService ctiService; - private static String ctiLoggerURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); + // private static String ctiLoggerURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); + @Value("${cti-logger_base_url}") + private String ctiLoggerURL; public CallCentreDataSyncImpl() { if (httpUtils == null) { From a1d227cbc9ff410dec54a6264643a31901516114 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 4 Aug 2026 14:17:04 +0530 Subject: [PATCH 3/5] fix: update the properties --- .../service/callhandling/BeneficiaryCallServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/callhandling/BeneficiaryCallServiceImpl.java b/src/main/java/com/iemr/common/service/callhandling/BeneficiaryCallServiceImpl.java index c28d41cb..637d7ae4 100644 --- a/src/main/java/com/iemr/common/service/callhandling/BeneficiaryCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/callhandling/BeneficiaryCallServiceImpl.java @@ -145,7 +145,11 @@ public class BeneficiaryCallServiceImpl implements BeneficiaryCallService { private Logger logger = LoggerFactory.getLogger(BeneficiaryCallServiceImpl.class); - private static String ctiLoggerURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); + // private static String ctiLoggerURL = ConfigProperties.getPropertyByName("cti-logger_base_url"); + + @Value("${cti-logger_base_url}") + private String ctiLoggerURL; + @Autowired private IdentityBeneficiaryService identityBeneficiaryService; From a08912e5871258dbf463dffed5a018d7a41ce074 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 4 Aug 2026 14:17:28 +0530 Subject: [PATCH 4/5] fix: update the pom version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b4e8c0d9..acdb4fce 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.9.0 war Common-API From 92b3f200bd6ba2539ca5efa179952d14acd553bf Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 5 Aug 2026 14:08:32 +0530 Subject: [PATCH 5/5] fix: add the range to sync --- .../environment/common_example.properties | 5 + .../NHM_DashboardServiceImpl.java | 142 ++++++++++++++---- 2 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 8b23d6f5..8e3d969d 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -95,6 +95,11 @@ cron-scheduler-everwelldatasync=0 0/5 * * * ? * start-nhmdashboard-scheduler=true cron-scheduler-nhmdashboard=0 1 0 * * ? * nhm-detailedcallreport-backfill-days=7 +# one-off recovery of older / partly imported days (yyyy-MM-dd, both inclusive, +# max 60 days per run). Leave empty during normal operation - while these are set +# the job pulls this range instead of only the missing days. +nhm-detailedcallreport-backfill-start-date= +nhm-detailedcallreport-backfill-end-date= ##----------------------------------------------------#grievance data sync----------------------------------------------------------- start-grievancedatasync-scheduler=false diff --git a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java index 3360cdd7..d86c1ad9 100644 --- a/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java +++ b/src/main/java/com/iemr/common/service/nhm_dashboard/NHM_DashboardServiceImpl.java @@ -75,6 +75,14 @@ public class NHM_DashboardServiceImpl implements NHM_DashboardService { @Value("${nhm-detailedcallreport-backfill-days:7}") private int detailedCallReportBackfillDays; + @Value("${nhm-detailedcallreport-backfill-start-date:}") + private String backfillStartDate; + + @Value("${nhm-detailedcallreport-backfill-end-date:}") + private String backfillEndDate; + + private static final int MAX_EXPLICIT_BACKFILL_DAYS = 60; + public String pushAbandonCalls(AbandonCallSummary abandonCallSummary) throws Exception { logger.info("NHM_abandon call push API request : " + abandonCallSummary.toString()); @@ -129,7 +137,6 @@ public String getDetailedCallReport() throws Exception { return new Gson().toJson(resultSet); } - // JOB calling C-Zentrix 2 APIs => AgentSummaryReport & DetailedCallReport public String pull_NHM_Data_CTI() throws IEMRException { String response = ""; String result1 = ""; @@ -145,14 +152,12 @@ public String pull_NHM_Data_CTI() throws IEMRException { } StringBuilder detailedCallReportResult = new StringBuilder(); - // each pending day is pulled separately, so that one failing day does not stop - // the remaining days for (LocalDate callDate : getPendingDetailedCallReportDates()) { try { List detailedCallReportList = callDetailedCallReportCTI_API(callDate); if (detailedCallReportList.size() > 0) { detailedCallReportResult.append(callDate).append(" : ") - .append(saveDetailedCallReport(detailedCallReportList)).append("; "); + .append(saveNewDetailedCallReport(detailedCallReportList, callDate)).append("; "); } } catch (Exception e) { logger.error("DetailedCallReport pull failed for " + callDate + " - " + e.getLocalizedMessage()); @@ -163,23 +168,45 @@ public String pull_NHM_Data_CTI() throws IEMRException { return response.concat(result1).concat(" ").concat(result2); } - /** - * Days (oldest first) for which detailed call report data still has to be - * pulled from CTI - yesterday plus any earlier day within the backfill window - * that has no data at all. Without this, a day missed because CTI was down or - * throttled ("Please wait for 1 hour") was never requested again and stayed - * permanently missing from the report. - */ + List getPendingDetailedCallReportDates() { - LocalDate lastDate = LocalDate.now().minusDays(1); + LocalDate yesterday = LocalDate.now().minusDays(1); + + LocalDate explicitStart = parseBackfillDate(backfillStartDate, "start"); + LocalDate explicitEnd = parseBackfillDate(backfillEndDate, "end"); + if (explicitStart != null) { + LocalDate lastDate = explicitEnd != null ? explicitEnd : yesterday; + // today is still in progress, never pull it + if (lastDate.isAfter(yesterday)) + lastDate = yesterday; + if (lastDate.isBefore(explicitStart)) { + logger.error("Configured detailed call report backfill range is empty - start " + explicitStart + + " is after end " + lastDate + ", falling back to the missing day check"); + } else { + List explicitDates = new ArrayList<>(); + for (LocalDate date = explicitStart; !date.isAfter(lastDate); date = date.plusDays(1)) { + if (explicitDates.size() >= MAX_EXPLICIT_BACKFILL_DAYS) { + logger.warn("Configured detailed call report backfill range exceeds " + + MAX_EXPLICIT_BACKFILL_DAYS + " days - stopping at " + date.minusDays(1) + + ", move the start date forward and run again to continue"); + break; + } + explicitDates.add(date); + } + logger.info("DetailedCallReport configured backfill range " + explicitStart + " to " + lastDate + + " - pulling " + explicitDates.size() + " day(s)"); + return explicitDates; + } + } + int lookBackDays = detailedCallReportBackfillDays > 0 ? detailedCallReportBackfillDays : 1; - LocalDate firstDate = lastDate.minusDays(lookBackDays - 1L); + LocalDate firstDate = yesterday.minusDays(lookBackDays - 1L); Set existingDates = new HashSet<>(); try { List dates = detailedCallReportRepo.findExistingCallDates( Timestamp.valueOf(firstDate.atStartOfDay()), - Timestamp.valueOf(lastDate.atTime(LocalTime.MAX).withNano(0))); + Timestamp.valueOf(yesterday.atTime(LocalTime.MAX).withNano(0))); for (java.sql.Date date : dates) { if (date != null) existingDates.add(date.toLocalDate()); @@ -187,18 +214,69 @@ List getPendingDetailedCallReportDates() { } catch (Exception e) { // on any problem in gap detection, fall back to the previous behaviour logger.error("Error while detecting missing detailed call report dates - " + e.getLocalizedMessage()); - return Arrays.asList(lastDate); + return Arrays.asList(yesterday); } List pendingDates = new ArrayList<>(); - for (LocalDate date = firstDate; !date.isAfter(lastDate); date = date.plusDays(1)) { + for (LocalDate date = firstDate; !date.isAfter(yesterday); date = date.plusDays(1)) { if (!existingDates.contains(date)) pendingDates.add(date); } - logger.info("DetailedCallReport pending dates between " + firstDate + " and " + lastDate + " : " + pendingDates); + logger.info("DetailedCallReport pending dates between " + firstDate + " and " + yesterday + " : " + pendingDates); return pendingDates; } + private LocalDate parseBackfillDate(String value, String label) { + if (value == null || value.trim().isEmpty()) + return null; + try { + return LocalDate.parse(value.trim()); + } catch (Exception e) { + logger.error("Ignoring detailed call report backfill " + label + " date '" + value + + "' - expected format yyyy-MM-dd"); + return null; + } + } + + String saveNewDetailedCallReport(List detailedCallReportList, LocalDate callDate) + throws IEMRException { + parseDetailedCallReportTimestamps(detailedCallReportList); + + Set existingKeys = new HashSet<>(); + for (DetailedCallReport existing : detailedCallReportRepo.findByCallStartTimeBetween( + Timestamp.valueOf(callDate.atStartOfDay()), + Timestamp.valueOf(callDate.atTime(LocalTime.MAX).withNano(0)))) { + existingKeys.add(getDetailedCallReportKey(existing)); + } + + List newRecords = new ArrayList<>(); + for (DetailedCallReport detailedCallReport : detailedCallReportList) { + if (existingKeys.add(getDetailedCallReportKey(detailedCallReport))) + newRecords.add(detailedCallReport); + } + + int duplicates = detailedCallReportList.size() - newRecords.size(); + if (newRecords.isEmpty()) { + logger.info("DetailedCallReport " + callDate + " - all " + detailedCallReportList.size() + + " record(s) already present, nothing to save"); + return "0 records saved, " + duplicates + " already present"; + } + + List resultSet = (List) detailedCallReportRepo.saveAll(newRecords); + logger.info("DetailedCallReport " + callDate + " - pulled " + detailedCallReportList.size() + ", saved " + + resultSet.size() + ", already present " + duplicates); + return resultSet.size() + " records saved, " + duplicates + " already present"; + } + + /** + * Natural key of a call record. A session can hold more than one leg (transfer, + * redial), so the phone number and start time are part of the key as well. + */ + private String getDetailedCallReportKey(DetailedCallReport detailedCallReport) { + return String.valueOf(detailedCallReport.getSession_ID()) + '|' + detailedCallReport.getPHONE() + '|' + + detailedCallReport.getCallStartTime() + '|' + detailedCallReport.getAgent_ID(); + } + public String saveAgentSummaryReport(List agentSummaryReportList) throws IEMRException { List resultSet = (List) agentSummaryReportRepo @@ -210,7 +288,26 @@ public String saveAgentSummaryReport(List agentSummaryReport public String saveDetailedCallReport(List detailedCallReportList) throws IEMRException { if (detailedCallReportList != null && detailedCallReportList.size() > 0) { - for (DetailedCallReport detailedCallReport : detailedCallReportList) { + parseDetailedCallReportTimestamps(detailedCallReportList); + + List resultSet = (List) detailedCallReportRepo + .saveAll(detailedCallReportList); + + return resultSet.size() + " detailedCallReport records saved successfully"; + } else + throw new IEMRException("please pass valid DetailedCallReport data in list"); + } + + /** + * CTI sends the times as strings; they are moved into the timestamp columns + * here. Has to run before the records are compared against what is already + * stored, because the comparison uses the parsed start time. + */ + private void parseDetailedCallReportTimestamps(List detailedCallReportList) { + if (detailedCallReportList == null) + return; + + for (DetailedCallReport detailedCallReport : detailedCallReportList) { try { if (detailedCallReport.getCall_Start_Time() != null && !detailedCallReport.getCall_Start_Time().equalsIgnoreCase("0000-00-00 00:00:00")) @@ -244,14 +341,7 @@ public String saveDetailedCallReport(List detailedCallReport } catch (Exception e) { logger.error("Call_Start_Time" + e.getLocalizedMessage()); } - } - - List resultSet = (List) detailedCallReportRepo - .saveAll(detailedCallReportList); - - return resultSet.size() + " detailedCallReport records saved successfully"; - } else - throw new IEMRException("please pass valid DetailedCallReport data in list"); + } } public List callAgentSummaryReportCTI_API() throws IEMRException {