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 diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index ac440991..8e3d969d 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,13 @@ 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 +# 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/config/quartz/QuartzConfig.java b/src/main/java/com/iemr/common/config/quartz/QuartzConfig.java index 61e70fd0..cb6734b9 100644 --- a/src/main/java/com/iemr/common/config/quartz/QuartzConfig.java +++ b/src/main/java/com/iemr/common/config/quartz/QuartzConfig.java @@ -24,10 +24,12 @@ import java.io.IOException; import java.util.Properties; +import org.quartz.JobDetail; import org.quartz.Trigger; 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.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -38,8 +40,6 @@ import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; -import com.iemr.common.utils.config.ConfigProperties; - import jakarta.annotation.PostConstruct; @Configuration @@ -55,11 +55,80 @@ public class QuartzConfig { @Autowired private ApplicationContext applicationContext; + /* + * These are read through @Value and not through ConfigProperties on purpose. + * ConfigProperties keeps the Environment in a static field that is populated by + * an @Autowired setter on its own bean, so it can still be null while the @Bean + * methods below run - in that case it falls back to reading application.properties + * straight off the classpath, and any ${ENV_VAR} placeholder in there comes back + * as the literal text. getBoolean() then quietly turns that into false and the job + * is scheduled with quartzJobDefaultSchedule instead, with nothing in the log. + * Injected fields are set before any @Bean method is called and go through the + * normal placeholder resolution, so environment overrides are honoured. + */ + @Value("${start-unblock-scheduler:false}") + private boolean startUnblockJob; + @Value("${cron-scheduler-unblock:" + quartzJobDefaultSchedule + "}") + private String unblockSchedule; + + @Value("${start-sms-scheduler:false}") + private boolean startSmsJob; + @Value("${cron-scheduler-sms:" + quartzJobDefaultSchedule + "}") + private String smsSchedule; + + @Value("${start-email-scheduler:false}") + private boolean startEmailJob; + @Value("${cron-scheduler-email:" + quartzJobDefaultSchedule + "}") + private String emailSchedule; + + @Value("${start-registration-scheduler:false}") + private boolean startRegistrationJob; + @Value("${cron-scheduler-registration:" + quartzJobDefaultSchedule + "}") + private String registrationSchedule; + + @Value("${start-everwelldatasync-scheduler:false}") + private boolean startEverwellDataSyncJob; + @Value("${cron-scheduler-everwelldatasync:" + quartzJobDefaultSchedule + "}") + private String everwellDataSyncSchedule; + + @Value("${start-ctidatasync-scheduler:false}") + private boolean startCtiDataSyncJob; + @Value("${cron-scheduler-ctidatasync:" + quartzJobDefaultSchedule + "}") + private String ctiDataSyncSchedule; + + @Value("${start-avni-scheduler:false}") + private boolean startAvniRegistrationJob; + @Value("${cron-avni-registration:" + quartzJobDefaultSchedule + "}") + private String avniRegistrationSchedule; + + @Value("${start-nhmdashboard-scheduler:false}") + private boolean startNhmDashboardJob; + @Value("${cron-scheduler-nhmdashboard:" + quartzJobDefaultSchedule + "}") + private String nhmDashboardSchedule; + @PostConstruct public void init() { log.debug("QuartzConfig initialized."); } + /** + * Builds the trigger for a job, logging what it resolved to. A job that is + * switched off gets quartzJobDefaultSchedule, which only comes round on the 31st + * of December - so the log line is the only way to tell "off" apart from + * "misconfigured" without waiting until the end of the year. + */ + private CronTriggerFactoryBean cronTrigger(String jobName, boolean startJob, String schedule, + JobDetail jobDetail) { + String scheduleConfig = startJob ? schedule : quartzJobDefaultSchedule; + log.info("Quartz job {} - enabled: {}, cron: {}", jobName, startJob, scheduleConfig); + + CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); + cronTriggerFactoryBean.setJobDetail(jobDetail); + cronTriggerFactoryBean.setCronExpression(scheduleConfig); + cronTriggerFactoryBean.setGroup(quartzJobGroup); + return cronTriggerFactoryBean; + } + @Bean public Properties quartzProperties() { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); @@ -114,17 +183,7 @@ public JobDetailFactoryBean processMQJobForUnblock() { @Bean public CronTriggerFactoryBean processMQTriggerForUnblock() { - Boolean startJob = ConfigProperties.getBoolean("start-unblock-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - ; - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-unblock"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForUnblock().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("unblock", startUnblockJob, unblockSchedule, processMQJobForUnblock().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -139,17 +198,7 @@ public JobDetailFactoryBean processMQJobForSMS() { @Bean public CronTriggerFactoryBean processMQTriggerForSMS() { - Boolean startJob = ConfigProperties.getBoolean("start-sms-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - ; - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-sms"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForSMS().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("sms", startSmsJob, smsSchedule, processMQJobForSMS().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -164,16 +213,7 @@ public JobDetailFactoryBean processMQJobForEmail() { @Bean public CronTriggerFactoryBean processMQTriggerForEmail() { - Boolean startJob = ConfigProperties.getBoolean("start-email-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (Boolean.TRUE.equals(startJob)) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-email"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForEmail().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("email", startEmailJob, emailSchedule, processMQJobForEmail().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -188,16 +228,8 @@ public JobDetailFactoryBean processMQJobForRegistration() { @Bean public CronTriggerFactoryBean processMQTriggerForRegistration() { - Boolean startJob = ConfigProperties.getBoolean("start-registration-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-registration"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForRegistration().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("everwell-registration", startRegistrationJob, registrationSchedule, + processMQJobForRegistration().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -212,16 +244,8 @@ public JobDetailFactoryBean processMQJobForEverwellDataSync() { @Bean public CronTriggerFactoryBean processMQTriggerForEverwellDataSync() { - Boolean startJob = ConfigProperties.getBoolean("start-everwelldatasync-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-everwelldatasync"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForEverwellDataSync().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("everwell-datasync", startEverwellDataSyncJob, everwellDataSyncSchedule, + processMQJobForEverwellDataSync().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -236,16 +260,8 @@ public JobDetailFactoryBean processMQJobForCtiDataSync() { @Bean public CronTriggerFactoryBean processMQTriggerForCtiDataSync() { - Boolean startJob = ConfigProperties.getBoolean("start-ctidatasync-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-ctidatasync"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForCtiDataSync().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("cti-datasync", startCtiDataSyncJob, ctiDataSyncSchedule, + processMQJobForCtiDataSync().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -260,16 +276,8 @@ public JobDetailFactoryBean processMQJobForAvniRegistration() { @Bean public CronTriggerFactoryBean processMQTriggerForAvniRegistration() { - Boolean startJob = ConfigProperties.getBoolean("start-avni-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-avni-registration"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForAvniRegistration().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - return cronTriggerFactoryBean; + return cronTrigger("avni-registration", startAvniRegistrationJob, avniRegistrationSchedule, + processMQJobForAvniRegistration().getObject()); } // -------------------------------------------------------------------------------------------------------------- @@ -284,17 +292,8 @@ public JobDetailFactoryBean processMQJobForNHMDashboardData() { @Bean public CronTriggerFactoryBean processMQTriggerForNHMDashboardData() { - Boolean startJob = ConfigProperties.getBoolean("start-nhmdashboard-scheduler"); - CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); - String scheduleConfig = quartzJobDefaultSchedule; - if (startJob) { - scheduleConfig = ConfigProperties.getPropertyByName("cron-scheduler-nhmdashboard"); - } - cronTriggerFactoryBean.setJobDetail(processMQJobForNHMDashboardData().getObject()); - cronTriggerFactoryBean.setCronExpression(scheduleConfig); - cronTriggerFactoryBean.setGroup(quartzJobGroup); - - return cronTriggerFactoryBean; + return cronTrigger("nhm-dashboard", startNhmDashboardJob, nhmDashboardSchedule, + processMQJobForNHMDashboardData().getObject()); } } 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/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/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/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; 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..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) { @@ -81,10 +83,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 +98,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..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 @@ -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,21 @@ 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; + + @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()); @@ -119,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 = ""; @@ -134,17 +151,130 @@ 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(); + for (LocalDate callDate : getPendingDetailedCallReportDates()) { + try { + List detailedCallReportList = callDetailedCallReportCTI_API(callDate); + if (detailedCallReportList.size() > 0) { + detailedCallReportResult.append(callDate).append(" : ") + .append(saveNewDetailedCallReport(detailedCallReportList, callDate)).append("; "); + } + } catch (Exception e) { + logger.error("DetailedCallReport pull failed for " + callDate + " - " + e.getLocalizedMessage()); + } + } + result2 = detailedCallReportResult.toString(); + return response.concat(result1).concat(" ").concat(result2); + } + + + List getPendingDetailedCallReportDates() { + 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 = yesterday.minusDays(lookBackDays - 1L); + + Set existingDates = new HashSet<>(); + try { + List dates = detailedCallReportRepo.findExistingCallDates( + Timestamp.valueOf(firstDate.atStartOfDay()), + Timestamp.valueOf(yesterday.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(yesterday); } - return response.concat(result1).concat(" ").concat(result2); + List pendingDates = new ArrayList<>(); + 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 " + 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 { @@ -158,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")) @@ -192,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 { @@ -213,8 +355,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 +390,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();