@@ -75,6 +75,14 @@ public class NHM_DashboardServiceImpl implements NHM_DashboardService {
7575 @ Value ("${nhm-detailedcallreport-backfill-days:7}" )
7676 private int detailedCallReportBackfillDays ;
7777
78+ @ Value ("${nhm-detailedcallreport-backfill-start-date:}" )
79+ private String backfillStartDate ;
80+
81+ @ Value ("${nhm-detailedcallreport-backfill-end-date:}" )
82+ private String backfillEndDate ;
83+
84+ private static final int MAX_EXPLICIT_BACKFILL_DAYS = 60 ;
85+
7886 public String pushAbandonCalls (AbandonCallSummary abandonCallSummary ) throws Exception {
7987
8088 logger .info ("NHM_abandon call push API request : " + abandonCallSummary .toString ());
@@ -129,7 +137,6 @@ public String getDetailedCallReport() throws Exception {
129137 return new Gson ().toJson (resultSet );
130138 }
131139
132- // JOB calling C-Zentrix 2 APIs => AgentSummaryReport & DetailedCallReport
133140 public String pull_NHM_Data_CTI () throws IEMRException {
134141 String response = "" ;
135142 String result1 = "" ;
@@ -145,14 +152,12 @@ public String pull_NHM_Data_CTI() throws IEMRException {
145152 }
146153
147154 StringBuilder detailedCallReportResult = new StringBuilder ();
148- // each pending day is pulled separately, so that one failing day does not stop
149- // the remaining days
150155 for (LocalDate callDate : getPendingDetailedCallReportDates ()) {
151156 try {
152157 List <DetailedCallReport > detailedCallReportList = callDetailedCallReportCTI_API (callDate );
153158 if (detailedCallReportList .size () > 0 ) {
154159 detailedCallReportResult .append (callDate ).append (" : " )
155- .append (saveDetailedCallReport (detailedCallReportList )).append ("; " );
160+ .append (saveNewDetailedCallReport (detailedCallReportList , callDate )).append ("; " );
156161 }
157162 } catch (Exception e ) {
158163 logger .error ("DetailedCallReport pull failed for " + callDate + " - " + e .getLocalizedMessage ());
@@ -163,42 +168,115 @@ public String pull_NHM_Data_CTI() throws IEMRException {
163168 return response .concat (result1 ).concat (" " ).concat (result2 );
164169 }
165170
166- /**
167- * Days (oldest first) for which detailed call report data still has to be
168- * pulled from CTI - yesterday plus any earlier day within the backfill window
169- * that has no data at all. Without this, a day missed because CTI was down or
170- * throttled ("Please wait for 1 hour") was never requested again and stayed
171- * permanently missing from the report.
172- */
171+
173172 List <LocalDate > getPendingDetailedCallReportDates () {
174- LocalDate lastDate = LocalDate .now ().minusDays (1 );
173+ LocalDate yesterday = LocalDate .now ().minusDays (1 );
174+
175+ LocalDate explicitStart = parseBackfillDate (backfillStartDate , "start" );
176+ LocalDate explicitEnd = parseBackfillDate (backfillEndDate , "end" );
177+ if (explicitStart != null ) {
178+ LocalDate lastDate = explicitEnd != null ? explicitEnd : yesterday ;
179+ // today is still in progress, never pull it
180+ if (lastDate .isAfter (yesterday ))
181+ lastDate = yesterday ;
182+ if (lastDate .isBefore (explicitStart )) {
183+ logger .error ("Configured detailed call report backfill range is empty - start " + explicitStart
184+ + " is after end " + lastDate + ", falling back to the missing day check" );
185+ } else {
186+ List <LocalDate > explicitDates = new ArrayList <>();
187+ for (LocalDate date = explicitStart ; !date .isAfter (lastDate ); date = date .plusDays (1 )) {
188+ if (explicitDates .size () >= MAX_EXPLICIT_BACKFILL_DAYS ) {
189+ logger .warn ("Configured detailed call report backfill range exceeds "
190+ + MAX_EXPLICIT_BACKFILL_DAYS + " days - stopping at " + date .minusDays (1 )
191+ + ", move the start date forward and run again to continue" );
192+ break ;
193+ }
194+ explicitDates .add (date );
195+ }
196+ logger .info ("DetailedCallReport configured backfill range " + explicitStart + " to " + lastDate
197+ + " - pulling " + explicitDates .size () + " day(s)" );
198+ return explicitDates ;
199+ }
200+ }
201+
175202 int lookBackDays = detailedCallReportBackfillDays > 0 ? detailedCallReportBackfillDays : 1 ;
176- LocalDate firstDate = lastDate .minusDays (lookBackDays - 1L );
203+ LocalDate firstDate = yesterday .minusDays (lookBackDays - 1L );
177204
178205 Set <LocalDate > existingDates = new HashSet <>();
179206 try {
180207 List <java .sql .Date > dates = detailedCallReportRepo .findExistingCallDates (
181208 Timestamp .valueOf (firstDate .atStartOfDay ()),
182- Timestamp .valueOf (lastDate .atTime (LocalTime .MAX ).withNano (0 )));
209+ Timestamp .valueOf (yesterday .atTime (LocalTime .MAX ).withNano (0 )));
183210 for (java .sql .Date date : dates ) {
184211 if (date != null )
185212 existingDates .add (date .toLocalDate ());
186213 }
187214 } catch (Exception e ) {
188215 // on any problem in gap detection, fall back to the previous behaviour
189216 logger .error ("Error while detecting missing detailed call report dates - " + e .getLocalizedMessage ());
190- return Arrays .asList (lastDate );
217+ return Arrays .asList (yesterday );
191218 }
192219
193220 List <LocalDate > pendingDates = new ArrayList <>();
194- for (LocalDate date = firstDate ; !date .isAfter (lastDate ); date = date .plusDays (1 )) {
221+ for (LocalDate date = firstDate ; !date .isAfter (yesterday ); date = date .plusDays (1 )) {
195222 if (!existingDates .contains (date ))
196223 pendingDates .add (date );
197224 }
198- logger .info ("DetailedCallReport pending dates between " + firstDate + " and " + lastDate + " : " + pendingDates );
225+ logger .info ("DetailedCallReport pending dates between " + firstDate + " and " + yesterday + " : " + pendingDates );
199226 return pendingDates ;
200227 }
201228
229+ private LocalDate parseBackfillDate (String value , String label ) {
230+ if (value == null || value .trim ().isEmpty ())
231+ return null ;
232+ try {
233+ return LocalDate .parse (value .trim ());
234+ } catch (Exception e ) {
235+ logger .error ("Ignoring detailed call report backfill " + label + " date '" + value
236+ + "' - expected format yyyy-MM-dd" );
237+ return null ;
238+ }
239+ }
240+
241+ String saveNewDetailedCallReport (List <DetailedCallReport > detailedCallReportList , LocalDate callDate )
242+ throws IEMRException {
243+ parseDetailedCallReportTimestamps (detailedCallReportList );
244+
245+ Set <String > existingKeys = new HashSet <>();
246+ for (DetailedCallReport existing : detailedCallReportRepo .findByCallStartTimeBetween (
247+ Timestamp .valueOf (callDate .atStartOfDay ()),
248+ Timestamp .valueOf (callDate .atTime (LocalTime .MAX ).withNano (0 )))) {
249+ existingKeys .add (getDetailedCallReportKey (existing ));
250+ }
251+
252+ List <DetailedCallReport > newRecords = new ArrayList <>();
253+ for (DetailedCallReport detailedCallReport : detailedCallReportList ) {
254+ if (existingKeys .add (getDetailedCallReportKey (detailedCallReport )))
255+ newRecords .add (detailedCallReport );
256+ }
257+
258+ int duplicates = detailedCallReportList .size () - newRecords .size ();
259+ if (newRecords .isEmpty ()) {
260+ logger .info ("DetailedCallReport " + callDate + " - all " + detailedCallReportList .size ()
261+ + " record(s) already present, nothing to save" );
262+ return "0 records saved, " + duplicates + " already present" ;
263+ }
264+
265+ List <DetailedCallReport > resultSet = (List <DetailedCallReport >) detailedCallReportRepo .saveAll (newRecords );
266+ logger .info ("DetailedCallReport " + callDate + " - pulled " + detailedCallReportList .size () + ", saved "
267+ + resultSet .size () + ", already present " + duplicates );
268+ return resultSet .size () + " records saved, " + duplicates + " already present" ;
269+ }
270+
271+ /**
272+ * Natural key of a call record. A session can hold more than one leg (transfer,
273+ * redial), so the phone number and start time are part of the key as well.
274+ */
275+ private String getDetailedCallReportKey (DetailedCallReport detailedCallReport ) {
276+ return String .valueOf (detailedCallReport .getSession_ID ()) + '|' + detailedCallReport .getPHONE () + '|'
277+ + detailedCallReport .getCallStartTime () + '|' + detailedCallReport .getAgent_ID ();
278+ }
279+
202280 public String saveAgentSummaryReport (List <AgentSummaryReport > agentSummaryReportList ) throws IEMRException {
203281
204282 List <AgentSummaryReport > resultSet = (List <AgentSummaryReport >) agentSummaryReportRepo
@@ -210,7 +288,26 @@ public String saveAgentSummaryReport(List<AgentSummaryReport> agentSummaryReport
210288 public String saveDetailedCallReport (List <DetailedCallReport > detailedCallReportList ) throws IEMRException {
211289
212290 if (detailedCallReportList != null && detailedCallReportList .size () > 0 ) {
213- for (DetailedCallReport detailedCallReport : detailedCallReportList ) {
291+ parseDetailedCallReportTimestamps (detailedCallReportList );
292+
293+ List <DetailedCallReport > resultSet = (List <DetailedCallReport >) detailedCallReportRepo
294+ .saveAll (detailedCallReportList );
295+
296+ return resultSet .size () + " detailedCallReport records saved successfully" ;
297+ } else
298+ throw new IEMRException ("please pass valid DetailedCallReport data in list" );
299+ }
300+
301+ /**
302+ * CTI sends the times as strings; they are moved into the timestamp columns
303+ * here. Has to run before the records are compared against what is already
304+ * stored, because the comparison uses the parsed start time.
305+ */
306+ private void parseDetailedCallReportTimestamps (List <DetailedCallReport > detailedCallReportList ) {
307+ if (detailedCallReportList == null )
308+ return ;
309+
310+ for (DetailedCallReport detailedCallReport : detailedCallReportList ) {
214311 try {
215312 if (detailedCallReport .getCall_Start_Time () != null
216313 && !detailedCallReport .getCall_Start_Time ().equalsIgnoreCase ("0000-00-00 00:00:00" ))
@@ -244,14 +341,7 @@ public String saveDetailedCallReport(List<DetailedCallReport> detailedCallReport
244341 } catch (Exception e ) {
245342 logger .error ("Call_Start_Time" + e .getLocalizedMessage ());
246343 }
247- }
248-
249- List <DetailedCallReport > resultSet = (List <DetailedCallReport >) detailedCallReportRepo
250- .saveAll (detailedCallReportList );
251-
252- return resultSet .size () + " detailedCallReport records saved successfully" ;
253- } else
254- throw new IEMRException ("please pass valid DetailedCallReport data in list" );
344+ }
255345 }
256346
257347 public List <AgentSummaryReport > callAgentSummaryReportCTI_API () throws IEMRException {
0 commit comments