tango-app-api-infra 3.9.26 → 3.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import { aggregateClient, findOneClient } from '../services/client.service.js';
10
10
  import { findUser, findOneUser } from '../services/user.service.js';
11
11
  import { sendPushNotification } from 'tango-app-api-middleware';
12
12
  import { findStoreAccuracIssues, upsertStoreAccuracIssues } from '../services/storeAccuracyIssues.service.js';
13
+ import { updateOneUpsertVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
13
14
  dayjs.extend( utc );
14
15
  dayjs.extend( timezone );
15
16
 
@@ -146,7 +147,7 @@ export async function tangoReviewTicket( req, res ) {
146
147
  const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
147
148
  const hits = getFootfallCount?.body?.hits?.hits || [];
148
149
  if ( hits?.[0]?._source?.footfall_count <= 0 ) {
149
- return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
150
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data', 400 );
150
151
  }
151
152
  // get category details from the client level configuration
152
153
  const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
@@ -233,19 +234,11 @@ export async function tangoReviewTicket( req, res ) {
233
234
  return res.sendError( 'You don’t have any tagged images right now', 400 );
234
235
  }
235
236
 
236
-
237
- if ( inputData?.ticketType != 'internal' ) {
238
- const approveDueDate = ticketData?.[0]?._source?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.dueDate;
239
-
240
- if ( dayjs().isAfter( dayjs( approveDueDate ), 'day' ) ) {
241
- res.sendError( 'Ticket approval is not allowed beyond the due date.' );
242
- }
243
- }
244
237
  const tangoDueDate = ticketData?.[0]?._source?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.dueDate;
245
238
  // const tangoAllowDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
246
239
 
247
240
  if ( dayjs().isAfter( dayjs( tangoDueDate ), 'day' ) && inputData?.ticketType !== 'internal' ) {
248
- return res.sendError( `Tango review is not allowed beyond the due date.` );
241
+ return res.sendError( `Tango review is not allowed beyond the due date.`, 400 );
249
242
  }
250
243
 
251
244
  if ( inputData.ticketType === 'internal' ) {
@@ -279,6 +272,7 @@ export async function tangoReviewTicket( req, res ) {
279
272
  createdByEmail: req?.user?.email,
280
273
  createdByUserName: req?.user?.userName,
281
274
  createdByRole: req?.user?.role,
275
+ contactEmail: req?.user?.email,
282
276
  createdAt: new Date(),
283
277
  },
284
278
  );
@@ -297,6 +291,7 @@ export async function tangoReviewTicket( req, res ) {
297
291
  status: parseInt( inputData?.mappingInfo?.revicedPerc || 0 ) < parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview || 0 ) ? 'Open - Accuracy Issue' : 'Closed',
298
292
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
299
293
  revicedPerc: inputData.mappingInfo?.revicedPerc,
294
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
300
295
  mappingInfo: ticketData?.[0]?._source?.mappingInfo,
301
296
  createdByEmail: req?.user?.email,
302
297
  createdByUserName: req?.user?.userName,
@@ -314,6 +309,7 @@ export async function tangoReviewTicket( req, res ) {
314
309
  const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
315
310
  // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
316
311
  const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
312
+ const accuracy = getConfig?.footfallDirectoryConfigs?.accuracyBreach || null;
317
313
  if (
318
314
  isAutoCloseEnable === true &&
319
315
  revisedPercentage >= autoCloseAccuracyValue
@@ -329,6 +325,7 @@ export async function tangoReviewTicket( req, res ) {
329
325
  mode: inputData.mappingInfo?.mode,
330
326
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
331
327
  revicedPerc: inputData.mappingInfo?.revicedPerc,
328
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
332
329
  count: inputData.mappingInfo?.count,
333
330
  revisedDetail: inputData.mappingInfo?.revisedDetail,
334
331
  status: 'Closed',
@@ -358,12 +355,14 @@ export async function tangoReviewTicket( req, res ) {
358
355
  mode: inputData.mappingInfo?.mode,
359
356
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
360
357
  revicedPerc: inputData.mappingInfo?.revicedPerc,
358
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
361
359
  count: inputData.mappingInfo?.count,
362
360
  revisedDetail: inputData.mappingInfo?.revisedDetail,
363
361
  status: 'Closed',
364
362
  createdByEmail: req?.user?.email,
365
363
  createdByUserName: req?.user?.userName,
366
364
  createdByRole: req?.user?.role,
365
+ contactEmail: req?.user?.email,
367
366
  createdAt: new Date(),
368
367
  },
369
368
  );
@@ -416,6 +415,7 @@ export async function tangoReviewTicket( req, res ) {
416
415
  createdByEmail: req?.user?.email,
417
416
  createdByUserName: req?.user?.userName,
418
417
  createdByRole: req?.user?.role,
418
+ contactEmail: req?.user?.email,
419
419
  createdAt: new Date(),
420
420
  },
421
421
 
@@ -467,6 +467,7 @@ export async function tangoReviewTicket( req, res ) {
467
467
  createdByEmail: req?.user?.email,
468
468
  createdByUserName: req?.user?.userName,
469
469
  createdByRole: req?.user?.role,
470
+ contactEmail: req?.user?.email,
470
471
  createdAt: new Date(),
471
472
  },
472
473
  );
@@ -504,24 +505,310 @@ export async function tangoReviewTicket( req, res ) {
504
505
  // return;
505
506
 
506
507
  const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
507
-
508
508
  if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
509
- if ( ( record.status === 'Closed' || record.status === 'Open - Accuracy Issue' ) && inputData.ticketType !== 'internal' && tangoApproved ) {
509
+ if ( accuracy && accuracy.ticketCount && accuracy.days && tangoApproved ) {
510
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
511
+
512
+
513
+ const breachDays = Number( accuracy.days );
514
+ const breachCount = Number( accuracy.ticketCount );
515
+
516
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
517
+ // const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
518
+ const ticketName = 'footfall-directory'; // adjust if ticket name variable differs
519
+
520
+
521
+ const formatDate = ( d ) =>
522
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
523
+
524
+ // Compute current date object
525
+
526
+
527
+ // Calculate start date based on config days
528
+ // If 30 days: start from first day of current month
529
+ // If 60 days: start from first day of last month
530
+ const currentDateObj = new Date( inputData?.dateString ); // Declare currentDateObj as today's date
531
+ const startDateObj = new Date( currentDateObj );
532
+
533
+ if ( breachDays === 7 ) {
534
+ // Consider within this week (start from Monday)
535
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
536
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
537
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
538
+ } else if ( breachDays === 30 ) {
539
+ // Consider within this month
540
+ startDateObj.setDate( 1 ); // First day of current month
541
+ } else if ( breachDays === 60 ) {
542
+ // Consider this month and last month
543
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
544
+ startDateObj.setDate( 1 ); // First day of last month
545
+ } else {
546
+ // For other values, calculate months from days
547
+ const breachMonths = Math.ceil( breachDays / 30 );
548
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
549
+ startDateObj.setDate( 1 );
550
+ }
551
+
552
+
553
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
554
+ const startDate = formatDate( startDateObj );
555
+
556
+ // Calculate end date of the period (end of week/month) for dateString range
557
+ const endDateObj = new Date( currentDateObj );
558
+ if ( breachDays === 7 ) {
559
+ const dayOfWeek = endDateObj.getDay();
560
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
561
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
562
+ } else if ( breachDays === 30 ) {
563
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
564
+ } else if ( breachDays === 60 ) {
565
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
566
+ } else {
567
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
568
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
569
+ }
570
+ const endDate = formatDate( endDateObj );
571
+
572
+ // Query for tickets within this week/month window for this store and ticket name
573
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
574
+ const mappingInfoFilter = [
575
+ {
576
+ nested: {
577
+ path: 'mappingInfo',
578
+ query: {
579
+ bool: {
580
+ must: [
581
+ { term: { 'mappingInfo.type': 'approve' } },
582
+ { term: { 'mappingInfo.status': 'Closed' } },
583
+ ],
584
+ },
585
+ },
586
+ },
587
+ },
588
+ ];
589
+ if ( tangoApproved ) {
590
+ mappingInfoFilter.push( {
591
+ nested: {
592
+ path: 'mappingInfo',
593
+ query: {
594
+ term: { 'mappingInfo.type': 'tangoreview' },
595
+ },
596
+ },
597
+ } );
598
+ }
599
+ logger.info( { startDate, endDate, storeId: inputData?.storeId } );
510
600
  const query = {
511
- storeId: inputData?.storeId,
512
- isVideoStream: true,
601
+ query: {
602
+ bool: {
603
+ must: [
604
+ { term: { 'storeId.keyword': inputData?.storeId } },
605
+ { term: { 'ticketName.keyword': ticketName } },
606
+ { range: { dateString: { gte: startDate, lte: endDate } } },
607
+ ],
608
+ filter: [
609
+ {
610
+ bool: {
611
+ should: mappingInfoFilter,
612
+ minimum_should_match: 1,
613
+ },
614
+ },
615
+ ],
616
+ },
617
+ },
618
+ };
619
+ // Search in OpenSearch for recent similar tickets
620
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
621
+ logger.info( { breachTicketsResult, message: 'breachTicketsResult;;;;;;;1' } );
622
+ const getTickets = breachTicketsResult?.body?.hits?.hits || [];
623
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
624
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
625
+
626
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
627
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
628
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
629
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
630
+
631
+ const compareByOperator = ( value, operator, threshold ) => {
632
+ switch ( operator ) {
633
+ case '>': return value > threshold;
634
+ case '>=': return value >= threshold;
635
+ case '<': return value < threshold;
636
+ case '<=': return value <= threshold;
637
+ case '==': return value == threshold;
638
+ case '===': return value === threshold;
639
+ default: return value > threshold;
640
+ }
513
641
  };
514
- const getStoreType = await countDocumnetsCamera( query );
515
642
 
516
- // Get all tempIds from revopInfo response
517
- const temp = [];
518
- inputData?.mappingInfo?.revisedDetail?.map( ( hit ) => ( hit?.revopsType === 'duplicate' && hit?.isParent === true ) ? '':temp.push( { tempId: hit?.tempId } ) )|| [];
643
+ // Filter tickets where revised footfall percentage > config accuracy
644
+ let breachTicketsCount = 0;
645
+ for ( const ticketInfo of getTickets ) {
646
+ const source = ticketInfo._source || {};
519
647
 
520
- // Prepare management eyeZone query based on storeId and dateString
648
+ const ticketMappingInfo = source.mappingInfo || [];
521
649
 
522
- const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
523
- if ( isSendMessge === true ) {
524
- logger.info( '....1' );
650
+
651
+ // Get Store/AOM accuracy (tagging stage)
652
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
653
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
654
+ const storeRevisedFF = Number( taggingEntry?.revicedFootfall )|| 0;
655
+
656
+ // Get Central Ops(tangoreview stage)
657
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
658
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
659
+ const centralOpsRevisedFF = Number( centralOpsEntry?.revicedFootfall )|| 0;
660
+
661
+ // Get Central Ops/Tango accuracy (tangoreview stage)
662
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
663
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
664
+ const tangoRevisedFF = Number( tangoEntry?.revicedFootfall )|| 0;
665
+
666
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
667
+
668
+ const accuracyDelta1 = tangoAccuracy -storeAccuracy;
669
+ const accuracyDelta2 = centralOpsAccuracy - storeAccuracy;
670
+
671
+
672
+ // Check existing breach condition OR new accuracy delta breach conditions:
673
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
674
+ // New conditions (all must be true):
675
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
676
+ // b) Post-review (Tango) accuracy > 85%
677
+ // c) Total images marked for validation > taggingBreach config
678
+ // const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
679
+ const isHighDelta = compareByOperator( accuracyDelta1, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta2, deltaBreach, deltaCountBreach );
680
+ const isCrossingThreshold =source?.reviced > gettingTangoreview;
681
+ logger.info( { source, revicedData: source?.reviced, gettingTangoreview } );
682
+ const isLowData = compareByOperator( tangoRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach ) || compareByOperator( centralOpsRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach );
683
+
684
+ const deltaBreachCondition =
685
+ isHighDelta && isCrossingThreshold && isLowData;
686
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview) || totalImages <= 10;
687
+ if ( deltaBreachCondition ) {
688
+ breachTicketsCount++;
689
+ }
690
+ }
691
+
692
+ // process.exit( 0 );
693
+ if ( breachTicketsCount > breachCount ) {
694
+ const getStoreName = await findOneStore( { storeId: inputData?.storeId }, { storeName: 1, _id: 0 } );
695
+
696
+ // Calculate remaining future days in the config period
697
+ const futureDates = [];
698
+
699
+ // Calculate end date of config period
700
+ const configEndDateObj = new Date( currentDateObj );
701
+ if ( breachDays === 7 ) {
702
+ // End of current week (Sunday)
703
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
704
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
705
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
706
+ } else if ( breachDays === 30 ) {
707
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
708
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
709
+ } else if ( breachDays === 60 ) {
710
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
711
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
712
+ } else {
713
+ // For other values, add the remaining days
714
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
715
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
716
+ }
717
+
718
+ // Generate future dates from the day after current date until end of config period
719
+ const tomorrow = new Date( currentDateObj );
720
+ tomorrow.setDate( tomorrow.getDate() + 1 );
721
+
722
+ const dateIterator = new Date( tomorrow );
723
+ while ( dateIterator <= configEndDateObj ) {
724
+ futureDates.push( formatDate( dateIterator ) );
725
+ dateIterator.setDate( dateIterator.getDate() + 1 );
726
+ }
727
+
728
+ // Check past days within the period (from startDate to currentDate - 1);
729
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
730
+ const pastIterator = new Date( startDateObj );
731
+ while ( pastIterator < currentDateObj ) {
732
+ const dateStr = formatDate( pastIterator );
733
+ const pastQuery = {
734
+ query: {
735
+ bool: {
736
+ must: [
737
+ { term: { 'storeId.keyword': inputData?.storeId } },
738
+ { term: { dateString: dateStr } },
739
+ { term: { 'type.keyword': 'store' } },
740
+ ],
741
+ },
742
+ },
743
+ };
744
+
745
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
746
+ const foundHits = searchRes?.body?.hits?.hits || [];
747
+ const getStoreName = await findOneStore( { storeId: inputData?.storeId }, { storeName: 1 } );
748
+
749
+ if ( foundHits.length === 0 ) {
750
+ const record = {
751
+ clientId: inputData?.storeId?.split( '-' )[0],
752
+ storeId: inputData?.storeId,
753
+ storeName: getStoreName?.storeName,
754
+ dateString: dateStr,
755
+ status: 'block',
756
+ };
757
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: dateStr }, record );
758
+ }
759
+ pastIterator.setDate( pastIterator.getDate() + 1 );
760
+ }
761
+
762
+ // Insert a block record for each future date where no store record exists
763
+ for ( const futureDateString of futureDates ) {
764
+ const pastQuery = {
765
+ query: {
766
+ bool: {
767
+ must: [
768
+ { term: { 'storeId.keyword': inputData?.storeId } },
769
+ { term: { dateString: futureDateString } },
770
+ { term: { 'type.keyword': 'store' } },
771
+ ],
772
+ },
773
+ },
774
+ };
775
+
776
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
777
+ const foundHits = searchRes?.body?.hits?.hits || [];
778
+
779
+ if ( foundHits.length === 0 ) {
780
+ const record = {
781
+ clientId: inputData?.storeId?.split( '-' )[0],
782
+ storeId: inputData?.storeId,
783
+ storeName: getStoreName?.storeName,
784
+ dateString: futureDateString,
785
+ status: 'block',
786
+ };
787
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
788
+ }
789
+ }
790
+ }
791
+ }
792
+ if ( isAutoCloseEnable === true && revisedPercentage >= autoCloseAccuracyValue ) {
793
+ logger.info( 'Ticket auto-closed due to meeting auto-close accuracy threshold' );
794
+ } else {
795
+ if ( ( record.status === 'Closed' || record.status === 'Open - Accuracy Issue' ) && inputData.ticketType !== 'internal' && tangoApproved ) {
796
+ const query = {
797
+ storeId: inputData?.storeId,
798
+ isVideoStream: true,
799
+ };
800
+ const getStoreType = await countDocumnetsCamera( query );
801
+
802
+ // Get all tempIds from revopInfo response
803
+ const temp = [];
804
+ inputData?.mappingInfo?.revisedDetail?.map( ( hit ) => ( hit?.revopsType === 'duplicate' && hit?.isParent === true ) ? '':temp.push( { tempId: hit?.tempId } ) )|| [];
805
+
806
+ // Prepare management eyeZone query based on storeId and dateString
807
+
808
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
809
+ if ( isSendMessge === true ) {
810
+ logger.info( '....1' );
811
+ }
525
812
  }
526
813
  }
527
814
  return res.sendSuccess( 'Ticket closed successfully' );
@@ -1615,7 +1902,7 @@ export async function ticketSummary( req, res ) {
1615
1902
  closedTickets,
1616
1903
  dueToday: dueToday,
1617
1904
  Expired: expiredTickets,
1618
- ...( isApprover !== true? underTangoReview: undertangoTickets ),
1905
+ ...( isApprover !== true? { underTangoReview: undertangoTickets }:'' ),
1619
1906
  avgTicket: ticketPercentageAvg + '%',
1620
1907
  avgAccuracy: ticketAccuracy + '%',
1621
1908
  };
@@ -2073,7 +2360,7 @@ export async function ticketSummary( req, res ) {
2073
2360
  closedTickets,
2074
2361
  dueToday: dueToday,
2075
2362
  Expired: expiredTickets,
2076
- ...( isApprover !== true? underTangoReview: undertangoTickets ),
2363
+ ...( isApprover !== true? { underTangoReview: undertangoTickets } : '' ),
2077
2364
  avgTicket: ticketPercentageAvg + '%',
2078
2365
  avgAccuracy: ticketAccuracy + '%',
2079
2366
  };
@@ -2557,7 +2844,7 @@ export async function ticketList( req, res ) {
2557
2844
  const revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
2558
2845
  if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
2559
2846
  for ( const r of revisionArray ) {
2560
- if ( r.actionType === 'approver' && r.isChecked === false ) {
2847
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
2561
2848
  isApprover = true;
2562
2849
  }
2563
2850
  }
@@ -4020,7 +4307,9 @@ export async function ticketList( req, res ) {
4020
4307
  'Ticket ID': item?.ticketId,
4021
4308
  'Store Name': item?.storeName,
4022
4309
  'Store ID': item?.storeId,
4023
- 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4310
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4311
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4312
+ 'Ticket Raised On': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4024
4313
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4025
4314
  // 'Closed Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4026
4315
  'Due Date': ( () => {
@@ -4033,6 +4322,7 @@ export async function ticketList( req, res ) {
4033
4322
  }
4034
4323
  return '';
4035
4324
  } )(),
4325
+ 'Actual FF': item?.footfallCount,
4036
4326
  ...( function() {
4037
4327
  let counts = undefined;
4038
4328
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4060,14 +4350,15 @@ export async function ticketList( req, res ) {
4060
4350
  }
4061
4351
  return {};
4062
4352
  } )(),
4063
- 'Actual FF': item?.footfallCount,
4064
- // 'Revised FF': item?.revicedFootfall,
4353
+ 'Revised FF': item?.revicedFootfall,
4065
4354
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4066
4355
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4067
4356
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4068
4357
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4069
4358
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4070
4359
  // 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4360
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4361
+ // 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4071
4362
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4072
4363
 
4073
4364
  } );
@@ -4080,11 +4371,41 @@ export async function ticketList( req, res ) {
4080
4371
  ticketId: item?.ticketId,
4081
4372
  storeId: item?.storeId,
4082
4373
  storeName: item?.storeName,
4374
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4375
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4083
4376
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4084
4377
  issueDate: item?.dateString,
4085
4378
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4086
4379
  footfall: item?.footfallCount,
4087
- // revicedFootfall: item?.revicedFootfall,
4380
+ ...( function() {
4381
+ let counts = undefined;
4382
+ if ( Array.isArray( item.mappingInfo ) ) {
4383
+ // Priority: tangoreview > approve > review > tagging
4384
+ const targetTypes = [ 'approve', 'review', 'tagging' ];
4385
+ for ( let type of targetTypes ) {
4386
+ const found = item.mappingInfo.find( ( f ) => f && f.type === type && Array.isArray( f.count ) );
4387
+ if ( found ) {
4388
+ counts = found.count;
4389
+ break;
4390
+ }
4391
+ }
4392
+ }
4393
+ // Fallback to item.count if not found
4394
+ if ( !counts && Array.isArray( item.count ) ) {
4395
+ counts = item.count;
4396
+ }
4397
+ if ( Array.isArray( counts ) ) {
4398
+ return counts.reduce( ( acc, curr ) => {
4399
+ if ( curr && curr.name && typeof curr.value !== 'undefined' ) {
4400
+ acc[curr.name.replace( /\s/g, '' )] = curr.value;
4401
+ // acc[curr.name] = curr.value;
4402
+ }
4403
+ return acc;
4404
+ }, {} );
4405
+ }
4406
+ return {};
4407
+ } )(),
4408
+ revicedFootfall: item?.revicedFootfall,
4088
4409
  type: item.type || 'store',
4089
4410
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4090
4411
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
@@ -4092,6 +4413,8 @@ export async function ticketList( req, res ) {
4092
4413
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4093
4414
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4094
4415
  // tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4416
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4417
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4095
4418
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4096
4419
 
4097
4420
  } );
@@ -4106,9 +4429,9 @@ export async function ticketList( req, res ) {
4106
4429
  'Ticket ID': item?.ticketId,
4107
4430
  'Store Name': item?.storeName,
4108
4431
  'Store ID': item?.storeId,
4109
- 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ).format( 'DD MMM, YYYY' ) : '--',
4432
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4433
+ 'Ticket Raised On': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ).format( 'DD MMM, YYYY' ) : '--',
4110
4434
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4111
- 'Ticket Closed Date': item.status === 'Closed' || item.status === 'Tango Review Done' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4112
4435
  'Due Date': ( () => {
4113
4436
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
4114
4437
  if ( dueDate ) {
@@ -4119,6 +4442,8 @@ export async function ticketList( req, res ) {
4119
4442
  }
4120
4443
  return '';
4121
4444
  } )(),
4445
+ 'Actual FF': item?.footfallCount,
4446
+
4122
4447
  ...( function() {
4123
4448
  let counts = undefined;
4124
4449
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4146,14 +4471,18 @@ export async function ticketList( req, res ) {
4146
4471
  }
4147
4472
  return {};
4148
4473
  } )(),
4149
- 'Actual FF': item?.footfallCount,
4150
- 'Revised FF': item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.revicedFootfall : '--',
4474
+ 'Revised FF': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4151
4475
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4152
4476
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4153
- ...( isApprover !== true? 'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '' ),
4477
+ ...( isApprover !== true ?
4478
+ {
4479
+ 'Tango (Accuracy%)':
4480
+ item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4481
+ } :
4482
+ {} ),
4154
4483
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4484
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4155
4485
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4156
- 'Created by': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4157
4486
 
4158
4487
  } );
4159
4488
  }
@@ -4167,6 +4496,7 @@ export async function ticketList( req, res ) {
4167
4496
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4168
4497
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4169
4498
  issueDate: item?.dateString,
4499
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4170
4500
  footfall: item?.footfallCount,
4171
4501
  ...( function() {
4172
4502
  let counts = undefined;
@@ -4196,15 +4526,13 @@ export async function ticketList( req, res ) {
4196
4526
  }
4197
4527
  return {};
4198
4528
  } )(),
4199
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4200
4529
  type: item.type || 'store',
4530
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4201
4531
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4202
4532
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4203
- ...( isApprover !== true? item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc : '' ),
4204
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4205
-
4533
+ ...( isApprover !== true ? { tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--' } : {} ),
4206
4534
  status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4207
- closedDate: item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4535
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4208
4536
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4209
4537
  } );
4210
4538
  }
@@ -4220,9 +4548,9 @@ export async function ticketList( req, res ) {
4220
4548
  'Ticket ID': item?.ticketId,
4221
4549
  'Store ID': item?.storeId,
4222
4550
  'Store Name': item?.storeName,
4223
- 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4551
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4552
+ 'Ticket Raised On': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4224
4553
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4225
- 'Ticket Closed Date': item.status === 'Closed' || item.status === 'Tango Review Done' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4226
4554
 
4227
4555
  'Due Date': ( () => {
4228
4556
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
@@ -4234,6 +4562,8 @@ export async function ticketList( req, res ) {
4234
4562
  }
4235
4563
  return '';
4236
4564
  } )(),
4565
+ 'Actual FF': item?.footfallCount,
4566
+
4237
4567
  ...( function() {
4238
4568
  let counts = undefined;
4239
4569
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4261,14 +4591,18 @@ export async function ticketList( req, res ) {
4261
4591
  }
4262
4592
  return {};
4263
4593
  } )(),
4264
- 'Actual FF': item?.footfallCount,
4265
4594
  // 'Revised FF': item?.revicedFootfall,
4266
- 'Revised FF': item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.revicedFootfall : '--',
4267
- 'Ticket Closed Date': item.status === 'Closed' || item.status === 'Tango Review Done' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4595
+ 'Revised FF': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4268
4596
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4269
4597
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4270
- ...( ticketsApproveFeature !== true? 'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--' ),
4598
+ ...( isApprover !== true ?
4599
+ {
4600
+ 'Tango (Accuracy%)':
4601
+ item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4602
+ } :
4603
+ {} ),
4271
4604
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4605
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4272
4606
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4273
4607
 
4274
4608
  } );
@@ -4284,6 +4618,7 @@ export async function ticketList( req, res ) {
4284
4618
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4285
4619
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4286
4620
  issueDate: item?.dateString,
4621
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4287
4622
  footfall: item?.footfallCount,
4288
4623
  ...( function() {
4289
4624
  let counts = undefined;
@@ -4313,23 +4648,20 @@ export async function ticketList( req, res ) {
4313
4648
  }
4314
4649
  return {};
4315
4650
  } )(),
4316
- // revicedFootfall: item?.revicedFootfall,
4317
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4651
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4318
4652
  type: item.type || 'store',
4319
4653
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4320
4654
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4321
- ...( isApprover !== true? tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '' ),
4322
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4323
-
4655
+ ...( isApprover !== true ? { tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--' } : {} ),
4324
4656
  status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4325
- closedDate: item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4657
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4326
4658
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4327
4659
 
4328
4660
  } );
4329
4661
  }
4330
4662
  }
4331
4663
  } else if ( ticketsApproveFeature ) {
4332
- if ( inputData.isExport ) {
4664
+ if ( inputData?.isExport ) {
4333
4665
  const exportData = [];
4334
4666
  for ( let item of ticketListData ) {
4335
4667
  exportData.push( {
@@ -4337,7 +4669,9 @@ export async function ticketList( req, res ) {
4337
4669
  'Ticket ID': item?.ticketId,
4338
4670
  'Store Name': item?.storeName,
4339
4671
  'Store ID': item?.storeId,
4340
- 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4672
+ 'Created by': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4673
+
4674
+ 'Ticket Raised On': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt ).format( 'DD MMM, YYYY' ) : '',
4341
4675
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4342
4676
  'Due Date': ( () => {
4343
4677
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate;
@@ -4349,6 +4683,8 @@ export async function ticketList( req, res ) {
4349
4683
  }
4350
4684
  return '';
4351
4685
  } )(),
4686
+ 'Actual FF': item?.footfallCount,
4687
+
4352
4688
  ...( function() {
4353
4689
  let counts = undefined;
4354
4690
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4376,14 +4712,15 @@ export async function ticketList( req, res ) {
4376
4712
  }
4377
4713
  return {};
4378
4714
  } )(),
4379
- 'Actual FF': item?.footfallCount,
4380
4715
  'Revised FF': item?.revicedFootfall,
4381
4716
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4382
4717
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4383
4718
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4384
4719
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4385
4720
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4386
- 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4721
+ // 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4722
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4723
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4387
4724
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4388
4725
 
4389
4726
  } );
@@ -4396,10 +4733,40 @@ export async function ticketList( req, res ) {
4396
4733
  ticketId: item?.ticketId,
4397
4734
  storeId: item?.storeId,
4398
4735
  storeName: item?.storeName,
4736
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4737
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4399
4738
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4400
4739
  issueDate: item?.dateString,
4401
4740
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4402
4741
  footfall: item?.footfallCount,
4742
+ ...( function() {
4743
+ let counts = undefined;
4744
+ if ( Array.isArray( item.mappingInfo ) ) {
4745
+ // Priority: tangoreview > approve > review > tagging
4746
+ const targetTypes = [ 'approve', 'review', 'tagging' ];
4747
+ for ( let type of targetTypes ) {
4748
+ const found = item.mappingInfo.find( ( f ) => f && f.type === type && Array.isArray( f.count ) );
4749
+ if ( found ) {
4750
+ counts = found.count;
4751
+ break;
4752
+ }
4753
+ }
4754
+ }
4755
+ // Fallback to item.count if not found
4756
+ if ( !counts && Array.isArray( item.count ) ) {
4757
+ counts = item.count;
4758
+ }
4759
+ if ( Array.isArray( counts ) ) {
4760
+ return counts.reduce( ( acc, curr ) => {
4761
+ if ( curr && curr.name && typeof curr.value !== 'undefined' ) {
4762
+ acc[curr.name.replace( /\s/g, '' )] = curr.value;
4763
+ // acc[curr.name] = curr.value;
4764
+ }
4765
+ return acc;
4766
+ }, {} );
4767
+ }
4768
+ return {};
4769
+ } )(),
4403
4770
  revicedFootfall: item?.revicedFootfall,
4404
4771
  type: item.type || 'store',
4405
4772
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
@@ -4407,7 +4774,9 @@ export async function ticketList( req, res ) {
4407
4774
  approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4408
4775
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4409
4776
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4410
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4777
+ // tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4778
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4779
+ // ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4411
4780
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4412
4781
 
4413
4782
  } );
@@ -4723,6 +5092,27 @@ export async function getTickets( req, res ) {
4723
5092
  mappingObj.type !== 'tangoreview'
4724
5093
  ) {
4725
5094
  mappingObj.revisedDetail = processedRevopSources;
5095
+ } else if (
5096
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
5097
+ mappingObj.type === 'tangoreview' &&
5098
+ Array.isArray( mappingObj.revisedDetail )
5099
+ ) {
5100
+ const url = JSON.parse( process.env.URL );
5101
+ const OLD_CDN = url?.tangoReviewOld;
5102
+ const NEW_CDN =url?.tangoReviewNew; // bucket name : v4rnd-vms-audit-input-images
5103
+
5104
+ const replaceCdnInFilePath = ( detail ) => {
5105
+ if ( !detail || typeof detail !== 'object' ) return detail;
5106
+ if ( typeof detail.filePath === 'string' && detail.filePath.startsWith( OLD_CDN ) ) {
5107
+ detail.filePath = detail.filePath.replace( OLD_CDN, NEW_CDN );
5108
+ }
5109
+ if ( Array.isArray( detail.duplicateImage ) ) {
5110
+ detail.duplicateImage.forEach( replaceCdnInFilePath );
5111
+ }
5112
+ return detail;
5113
+ };
5114
+
5115
+ mappingObj.revisedDetail.forEach( replaceCdnInFilePath );
4726
5116
  }
4727
5117
  } );
4728
5118
  }
@@ -4743,7 +5133,14 @@ export async function getTickets( req, res ) {
4743
5133
 
4744
5134
  finalResponse[0]._source.mappingInfo[finalResponse?.[0]?._source.mappingInfo?.length - 1].revisedDetail = filterRevisedDetailByIsChecked( finalResponse?.[0]?._source?.mappingInfo[finalResponse?.[0]?._source?.mappingInfo?.length - 1].revisedDetail, isCheck );
4745
5135
  }
4746
- return res.sendSuccess( { result: finalResponse } );
5136
+
5137
+ // If tangoApproved is false, remove tangoreview entries from mappingInfo
5138
+ const clientId = finalResponse?.[0]?._source?.clientId;
5139
+
5140
+ const getConfig = clientId? await findOneClient( { clientId }, { footfallDirectoryConfigs: 1 } ): null;
5141
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
5142
+
5143
+ return res.sendSuccess( { result: finalResponse, tangoApproved: tangoApproved } );
4747
5144
  } catch ( error ) {
4748
5145
  const err = error.message || 'Internal Server Error';
4749
5146
  logger.error( { error: error, messgage: req.query } );
@@ -5075,6 +5472,29 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5075
5472
  sqsProduceQueue.MessageBody,
5076
5473
  );
5077
5474
  if ( sqsQueue.statusCode ) {
5475
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
5476
+ const logs = {
5477
+ QueueUrl: sqsProduceQueue.QueueUrl,
5478
+ MessageBody: sqsProduceQueue.MessageBody,
5479
+ sqsQueue: sqsQueue,
5480
+ statusCode: sqsQueue.statusCode,
5481
+ store_id: storeId,
5482
+ store_date: inputData.dateString.split( '-' ).reverse().join( '-' ),
5483
+ bucket_name: '',
5484
+ zone_id: 'traffic_zone',
5485
+ process_type: 'live',
5486
+ revop_type: 'footfall',
5487
+ temp_id: tempId,
5488
+ tempId1: inputData?.log?.temp1 || null,
5489
+ tempIds: inputData?.log?.tempIds || null,
5490
+ mapping: JSON.stringify( inputData?.log?.mapping ) || null,
5491
+ managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || null,
5492
+ managerEyeZoneQuery: inputData?.log?.managerEyeZoneQuery || null,
5493
+ time: Date.now(),
5494
+ createdAt: new Date(),
5495
+ createIST: dayjs().tz( 'Asia/Kolkata' ).format( 'YYYY-MM-DD' ),
5496
+ };
5497
+ await insertWithId( openSearch.revopsLog, id, logs );
5078
5498
  logger.error( {
5079
5499
  error: `${sqsQueue}`,
5080
5500
  type: 'UPLOAD_ERROR',
@@ -5093,11 +5513,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5093
5513
  process_type: 'live',
5094
5514
  revop_type: 'footfall',
5095
5515
  temp_id: tempId,
5096
- tempId1: inputData?.log?.temp1 || '',
5097
- tempIds: inputData?.log?.tempIds || '',
5098
- mapping: JSON.stringify( inputData?.log?.mapping ) || '',
5099
- managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || '',
5100
- managerEyeZoneQuery: inputData?.log?.managerEyeZoneQuery || '',
5516
+ tempId1: inputData?.log?.temp1 || null,
5517
+ tempIds: inputData?.log?.tempIds || null,
5518
+ mapping: JSON.stringify( inputData?.log?.mapping ) || null,
5519
+ managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || null,
5520
+ managerEyeZoneQuery: inputData?.log?.managerEyeZoneQuery || null,
5101
5521
  time: Date.now(),
5102
5522
  createdAt: new Date(),
5103
5523
  createIST: dayjs().tz( 'Asia/Kolkata' ).format( 'YYYY-MM-DD' ),
@@ -5865,7 +6285,8 @@ export async function updateTempStatus( req, res ) {
5865
6285
  isChecked = status === 'approved' ? true : false;
5866
6286
  break;
5867
6287
  case 'approve':
5868
- isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
6288
+ const reviewAction = actions.find( ( item ) => item.actionType === 'review' )?.action;
6289
+ isChecked = ( reviewAction === 'approved' ) === ( status === 'approved' );
5869
6290
  break;
5870
6291
  }
5871
6292
  docsToUpdate.push( {
@@ -6102,6 +6523,7 @@ export async function multiCloseTicket( req, res ) {
6102
6523
  const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
6103
6524
  const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
6104
6525
  const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
6526
+ const accuracy = getConfig?.footfallDirectoryConfigs?.accuracyBreach || null;
6105
6527
 
6106
6528
  // inputData structure should include an array of items to close
6107
6529
  // Accept both array or single ticket update
@@ -6211,6 +6633,7 @@ export async function multiCloseTicket( req, res ) {
6211
6633
  createdByEmail: req?.user?.email,
6212
6634
  createdByUserName: req?.user?.userName,
6213
6635
  createdByRole: req?.user?.role,
6636
+ contactEmail: req?.user?.email,
6214
6637
  createdAt: new Date(),
6215
6638
  updatedAt: new Date(),
6216
6639
  },
@@ -6255,6 +6678,7 @@ export async function multiCloseTicket( req, res ) {
6255
6678
  createdByEmail: req?.user?.email,
6256
6679
  createdByUserName: req?.user?.userName,
6257
6680
  createdByRole: req?.user?.role,
6681
+ contactEmail: req?.user?.email,
6258
6682
  createdAt: new Date(),
6259
6683
  updatedAt: new Date(),
6260
6684
  },
@@ -6270,7 +6694,7 @@ export async function multiCloseTicket( req, res ) {
6270
6694
  const ticketUpdatePayload = {
6271
6695
  doc: {
6272
6696
  mappingInfo: newMappingInfoArray,
6273
- status: ( revised < tangoReview && tangoApproved )? 'Under Tango Review': !tangoApproved? 'Closed': 'Approver-Closed', // status updated at top level as well
6697
+ status: ( revised < tangoReview && tangoApproved )? 'Under Tango Review': 'Closed', // status updated at top level as well
6274
6698
  updatedAt: new Date(),
6275
6699
  },
6276
6700
  };
@@ -6282,6 +6706,291 @@ export async function multiCloseTicket( req, res ) {
6282
6706
  results.push( { storeId, dateString, success: false, error: `Failed to update ticket this store ${storeId} for this date ${dateString}` } );
6283
6707
  continue;
6284
6708
  } else {
6709
+ if ( accuracy && accuracy.ticketCount && accuracy.days ) {
6710
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
6711
+
6712
+
6713
+ const breachDays = Number( accuracy.days );
6714
+ const breachCount = Number( accuracy.ticketCount );
6715
+
6716
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
6717
+ // const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
6718
+ const ticketName = 'footfall-directory'; // adjust if ticket name variable differs
6719
+
6720
+
6721
+ const formatDate = ( d ) =>
6722
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
6723
+
6724
+ // Compute current date object
6725
+
6726
+
6727
+ // Calculate start date based on config days
6728
+ // If 30 days: start from first day of current month
6729
+ // If 60 days: start from first day of last month
6730
+ const currentDateObj = new Date( dateString ); // Declare currentDateObj as today's date
6731
+ const startDateObj = new Date( currentDateObj );
6732
+
6733
+ if ( breachDays === 7 ) {
6734
+ // Consider within this week (start from Monday)
6735
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
6736
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
6737
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
6738
+ } else if ( breachDays === 30 ) {
6739
+ // Consider within this month
6740
+ startDateObj.setDate( 1 ); // First day of current month
6741
+ } else if ( breachDays === 60 ) {
6742
+ // Consider this month and last month
6743
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
6744
+ startDateObj.setDate( 1 ); // First day of last month
6745
+ } else {
6746
+ // For other values, calculate months from days
6747
+ const breachMonths = Math.ceil( breachDays / 30 );
6748
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
6749
+ startDateObj.setDate( 1 );
6750
+ }
6751
+
6752
+
6753
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
6754
+ const startDate = formatDate( startDateObj );
6755
+
6756
+ // Calculate end date of the period (end of week/month) for dateString range
6757
+ const endDateObj = new Date( currentDateObj );
6758
+ if ( breachDays === 7 ) {
6759
+ const dayOfWeek = endDateObj.getDay();
6760
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
6761
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
6762
+ } else if ( breachDays === 30 ) {
6763
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
6764
+ } else if ( breachDays === 60 ) {
6765
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
6766
+ } else {
6767
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
6768
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
6769
+ }
6770
+ const endDate = formatDate( endDateObj );
6771
+
6772
+ // Query for tickets within this week/month window for this store and ticket name
6773
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
6774
+ const mappingInfoFilter = [
6775
+ {
6776
+ nested: {
6777
+ path: 'mappingInfo',
6778
+ query: {
6779
+ bool: {
6780
+ must: [
6781
+ { term: { 'mappingInfo.type': 'approve' } },
6782
+ { term: { 'mappingInfo.status': 'Closed' } },
6783
+ ],
6784
+ },
6785
+ },
6786
+ },
6787
+ },
6788
+ ];
6789
+ if ( tangoApproved ) {
6790
+ mappingInfoFilter.push( {
6791
+ nested: {
6792
+ path: 'mappingInfo',
6793
+ query: {
6794
+ term: { 'mappingInfo.type': 'tangoreview' },
6795
+ },
6796
+ },
6797
+ } );
6798
+ }
6799
+ logger.info( { startDate, endDate, storeId } );
6800
+ const query = {
6801
+ query: {
6802
+ bool: {
6803
+ must: [
6804
+ { term: { 'storeId.keyword': storeId } },
6805
+ { term: { 'ticketName.keyword': ticketName } },
6806
+ { range: { dateString: { gte: startDate, lte: endDate } } },
6807
+ ],
6808
+ filter: [
6809
+ {
6810
+ bool: {
6811
+ should: mappingInfoFilter,
6812
+ minimum_should_match: 1,
6813
+ },
6814
+ },
6815
+ ],
6816
+ },
6817
+ },
6818
+ };
6819
+ // Search in OpenSearch for recent similar tickets
6820
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
6821
+ logger.info( { breachTicketsResult, message: 'breachTicketsResult;;;;;;;1' } );
6822
+ const getTickets = breachTicketsResult?.body?.hits?.hits || [];
6823
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
6824
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
6825
+
6826
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
6827
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
6828
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
6829
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
6830
+
6831
+ const compareByOperator = ( value, operator, threshold ) => {
6832
+ switch ( operator ) {
6833
+ case '>': return value > threshold;
6834
+ case '>=': return value >= threshold;
6835
+ case '<': return value < threshold;
6836
+ case '<=': return value <= threshold;
6837
+ case '==': return value == threshold;
6838
+ case '===': return value === threshold;
6839
+ default: return value > threshold;
6840
+ }
6841
+ };
6842
+
6843
+ // Filter tickets where revised footfall percentage > config accuracy
6844
+ let breachTicketsCount = 0;
6845
+ logger.info( { getTickets, message: 'tickets;;;;;;;1' } );
6846
+ for ( const ticketInfo of getTickets ) {
6847
+ const source = ticketInfo._source || {};
6848
+
6849
+ const ticketMappingInfo = source.mappingInfo || [];
6850
+
6851
+
6852
+ // Get Store/AOM accuracy (tagging stage)
6853
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
6854
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
6855
+ const storeRevisedFF = Number( taggingEntry?.revicedFootfall )|| 0;
6856
+
6857
+ // Get Central Ops(tangoreview stage)
6858
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
6859
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
6860
+ const centralOpsRevisedFF = Number( centralOpsEntry?.revicedFootfall )|| 0;
6861
+
6862
+ // Get Central Ops/Tango accuracy (tangoreview stage)
6863
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
6864
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
6865
+ const tangoRevisedFF = Number( tangoEntry?.revicedFootfall )|| 0;
6866
+
6867
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
6868
+
6869
+ const accuracyDelta1 = tangoAccuracy -storeAccuracy;
6870
+ const accuracyDelta2 = centralOpsAccuracy - storeAccuracy;
6871
+
6872
+
6873
+ // Check existing breach condition OR new accuracy delta breach conditions:
6874
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
6875
+ // New conditions (all must be true):
6876
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
6877
+ // b) Post-review (Tango) accuracy > 85%
6878
+ // c) Total images marked for validation > taggingBreach config
6879
+ // const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
6880
+ const isHighDelta = compareByOperator( accuracyDelta1, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta2, deltaBreach, deltaCountBreach );
6881
+ const isCrossingThreshold =source?.reviced > gettingTangoreview;
6882
+ logger.info( { source, revicedData: source?.reviced, gettingTangoreview } );
6883
+ const isLowData = compareByOperator( tangoRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach ) || compareByOperator( centralOpsRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach );
6884
+
6885
+ const deltaBreachCondition =
6886
+ isHighDelta && isCrossingThreshold && isLowData;
6887
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview) || totalImages <= 10;
6888
+ logger.info( { isHighDelta, isCrossingThreshold, isLowData } );
6889
+ if ( deltaBreachCondition ) {
6890
+ breachTicketsCount++;
6891
+ }
6892
+ }
6893
+ logger.info( { breachTicketsCount, breachCount }, 'total Breach Tickets Count1' );
6894
+
6895
+ // process.exit( 0 );
6896
+ if ( breachTicketsCount > breachCount ) {
6897
+ const getStoreName = await findOneStore( { storeId: storeId }, { storeName: 1, _id: 0 } );
6898
+
6899
+ // Calculate remaining future days in the config period
6900
+ const futureDates = [];
6901
+
6902
+ // Calculate end date of config period
6903
+ const configEndDateObj = new Date( currentDateObj );
6904
+ if ( breachDays === 7 ) {
6905
+ // End of current week (Sunday)
6906
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
6907
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
6908
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
6909
+ } else if ( breachDays === 30 ) {
6910
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
6911
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
6912
+ } else if ( breachDays === 60 ) {
6913
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
6914
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
6915
+ } else {
6916
+ // For other values, add the remaining days
6917
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
6918
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
6919
+ }
6920
+
6921
+ // Generate future dates from the day after current date until end of config period
6922
+ const tomorrow = new Date( currentDateObj );
6923
+ tomorrow.setDate( tomorrow.getDate() + 1 );
6924
+
6925
+ const dateIterator = new Date( tomorrow );
6926
+ while ( dateIterator <= configEndDateObj ) {
6927
+ futureDates.push( formatDate( dateIterator ) );
6928
+ dateIterator.setDate( dateIterator.getDate() + 1 );
6929
+ }
6930
+
6931
+ // Check past days within the period (from startDate to currentDate - 1);
6932
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
6933
+ const pastIterator = new Date( startDateObj );
6934
+ while ( pastIterator < currentDateObj ) {
6935
+ const dateStr = formatDate( pastIterator );
6936
+ const pastQuery = {
6937
+ query: {
6938
+ bool: {
6939
+ must: [
6940
+ { term: { 'storeId.keyword': storeId } },
6941
+ { term: { dateString: dateStr } },
6942
+ { term: { 'type.keyword': 'store' } },
6943
+ ],
6944
+ },
6945
+ },
6946
+ };
6947
+
6948
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
6949
+ const foundHits = searchRes?.body?.hits?.hits || [];
6950
+
6951
+ if ( foundHits.length === 0 ) {
6952
+ const record = {
6953
+ clientId: inputData?.ticketList?.[0]?.storeId?.split( '-' )[0],
6954
+ storeId: storeId,
6955
+ storeName: getStoreName?.storeName,
6956
+ dateString: dateStr,
6957
+ status: 'block',
6958
+ };
6959
+ await updateOneUpsertVmsStoreRequest( { storeId: storeId, dateString: dateStr }, record );
6960
+ }
6961
+ pastIterator.setDate( pastIterator.getDate() + 1 );
6962
+ }
6963
+
6964
+ // Insert a block record for each future date where no store record exists
6965
+ for ( const futureDateString of futureDates ) {
6966
+ const pastQuery = {
6967
+ query: {
6968
+ bool: {
6969
+ must: [
6970
+ { term: { 'storeId.keyword': storeId } },
6971
+ { term: { dateString: futureDateString } },
6972
+ { term: { 'type.keyword': 'store' } },
6973
+ ],
6974
+ },
6975
+ },
6976
+ };
6977
+
6978
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
6979
+ const foundHits = searchRes?.body?.hits?.hits || [];
6980
+
6981
+ if ( foundHits.length === 0 ) {
6982
+ const record = {
6983
+ clientId: inputData?.ticketList?.[0]?.storeId?.split( '-' )[0],
6984
+ storeId: storeId,
6985
+ storeName: getStoreName?.storeName,
6986
+ dateString: futureDateString,
6987
+ status: 'block',
6988
+ };
6989
+ await updateOneUpsertVmsStoreRequest( { storeId: storeId, dateString: futureDateString }, record );
6990
+ }
6991
+ }
6992
+ }
6993
+ }
6285
6994
  const query = {
6286
6995
  storeId: storeId,
6287
6996
  isVideoStream: true,
@@ -6512,3 +7221,376 @@ export async function updateAccuracyIssues( req, res ) {
6512
7221
  return res.sendError( err, 500 );
6513
7222
  }
6514
7223
  }
7224
+
7225
+ export async function getBadTicket( req, res ) {
7226
+ try {
7227
+ if ( accuracy && accuracy.ticketCount && accuracy.days && accuracy.accuracy ) {
7228
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
7229
+
7230
+ const breachDays = Number( accuracy.days );
7231
+ const breachCount = Number( accuracy.ticketCount );
7232
+
7233
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
7234
+ const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
7235
+ const storeId = inputData.storeId;
7236
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
7237
+
7238
+
7239
+ const formatDate = ( d ) =>
7240
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
7241
+
7242
+ // Compute current date object
7243
+
7244
+
7245
+ // Calculate start date based on config days
7246
+ // If 30 days: start from first day of current month
7247
+ // If 60 days: start from first day of last month
7248
+ const currentDateObj = new Date( inputData?.dateString ); // Declare currentDateObj as today's date
7249
+ const startDateObj = new Date( currentDateObj );
7250
+
7251
+ if ( breachDays === 7 ) {
7252
+ // Consider within this week (start from Monday)
7253
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
7254
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
7255
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
7256
+ } else if ( breachDays === 30 ) {
7257
+ // Consider within this month
7258
+ startDateObj.setDate( 1 ); // First day of current month
7259
+ } else if ( breachDays === 60 ) {
7260
+ // Consider this month and last month
7261
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
7262
+ startDateObj.setDate( 1 ); // First day of last month
7263
+ } else {
7264
+ // For other values, calculate months from days
7265
+ const breachMonths = Math.ceil( breachDays / 30 );
7266
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
7267
+ startDateObj.setDate( 1 );
7268
+ }
7269
+
7270
+
7271
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
7272
+ const startDate = formatDate( startDateObj );
7273
+
7274
+ // Calculate end date of the period (end of week/month) for dateString range
7275
+ const endDateObj = new Date( currentDateObj );
7276
+ if ( breachDays === 7 ) {
7277
+ const dayOfWeek = endDateObj.getDay();
7278
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
7279
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
7280
+ } else if ( breachDays === 30 ) {
7281
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
7282
+ } else if ( breachDays === 60 ) {
7283
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
7284
+ } else {
7285
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
7286
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
7287
+ }
7288
+ const endDate = formatDate( endDateObj );
7289
+
7290
+ // Query for tickets within this week/month window for this store and ticket name
7291
+ const query = {
7292
+ query: {
7293
+ bool: {
7294
+ must: [
7295
+ { term: { 'storeId.keyword': storeId } },
7296
+ { term: { 'ticketName.keyword': ticketName } },
7297
+ { range: { dateString: { gte: startDate, lte: endDate } } },
7298
+ ],
7299
+ must_not: [
7300
+ {
7301
+ terms: { 'status.keyword': [ 'Auto-Closed', 'Reviewer-Expired' ] },
7302
+ },
7303
+ ],
7304
+ },
7305
+ },
7306
+ };
7307
+
7308
+ // Search in OpenSearch for recent similar tickets
7309
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
7310
+ logger.info( { breachTicketsResult } );
7311
+ const tickets = breachTicketsResult?.body?.hits?.hits || [];
7312
+
7313
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
7314
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
7315
+
7316
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
7317
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
7318
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
7319
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
7320
+
7321
+ const compareByOperator = ( value, operator, threshold ) => {
7322
+ switch ( operator ) {
7323
+ case '>': return value > threshold;
7324
+ case '>=': return value >= threshold;
7325
+ case '<': return value < threshold;
7326
+ case '<=': return value <= threshold;
7327
+ case '==': return value == threshold;
7328
+ case '===': return value === threshold;
7329
+ default: return value > threshold;
7330
+ }
7331
+ };
7332
+
7333
+ // Filter tickets where revised footfall percentage > config accuracy
7334
+
7335
+ let breachTicketsCount = 0;
7336
+ for ( const ticket of tickets ) {
7337
+ const source = ticket._source || {};
7338
+
7339
+ const ticketFootfallCount = source.footfallCount || 0;
7340
+ const status = source.status || '';
7341
+ const ticketMappingInfo = source.mappingInfo || [];
7342
+ // Calculate revised footfall percentage
7343
+ const ticketRevisedPercentage = source.reviced;
7344
+ // Get Store/AOM accuracy (tagging stage)
7345
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
7346
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
7347
+ // Get AOM accuracy (tagging stage)
7348
+ const reviewEntry = ticketMappingInfo.find( ( m ) => m.type === 'review' );
7349
+ const AOMAccuracy = Number( reviewEntry?.reviced ) || 0;
7350
+
7351
+ // Get Central Ops(tangoreview stage)
7352
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
7353
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
7354
+
7355
+ // Get Central Ops/Tango accuracy (tangoreview stage)
7356
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
7357
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
7358
+
7359
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
7360
+ const accuracyDelta1 = AOMAccuracy - storeAccuracy;
7361
+ const accuracyDelta2 = tangoAccuracy - centralOpsAccuracy;
7362
+ const accuracyDelta3 = tangoAccuracy -storeAccuracy;
7363
+ const accuracyDelta4 = centralOpsAccuracy - AOMAccuracy;
7364
+ const accuracyDelta5 = tangoAccuracy -AOMAccuracy;
7365
+ const accuracyDelta6 = centralOpsAccuracy - storeAccuracy;
7366
+ // Get total images (footfall) marked for validation
7367
+ const totalImages =ticketFootfallCount || 0;
7368
+
7369
+ // Check existing breach condition OR new accuracy delta breach conditions:
7370
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
7371
+ // New conditions (all must be true):
7372
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
7373
+ // b) Post-review (Tango) accuracy > 85%
7374
+ // c) Total images marked for validation > taggingBreach config
7375
+ const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
7376
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview )|| totalImages <= 10;
7377
+ const isHighDelta = compareByOperator( accuracyDelta1, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta2, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta3, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta4, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta5, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta6, deltaBreach, deltaCountBreach );
7378
+
7379
+ const isCrossingThreshold =
7380
+ storeAccuracy < gettingTangoreview &&(
7381
+ tangoAccuracy > gettingTangoreview || centralOpsAccuracy > gettingTangoreview || AOMAccuracy > gettingTangoreview );
7382
+ const isLowData = compareByOperator( totalImages, taggingBreach, taggingCountBreach );
7383
+
7384
+ const deltaBreachCondition =
7385
+ isHighDelta || isCrossingThreshold || isLowData;
7386
+ if ( existingBreach || deltaBreachCondition ) {
7387
+ breachTicketsCount++;
7388
+ }
7389
+ }
7390
+ logger.info( { breachTicketsCount, breachCount } );
7391
+ if ( breachTicketsCount > breachCount ) {
7392
+ // Calculate remaining future days in the config period
7393
+ const futureDates = [];
7394
+
7395
+ // Calculate end date of config period
7396
+ const configEndDateObj = new Date( currentDateObj );
7397
+ if ( breachDays === 7 ) {
7398
+ // End of current week (Sunday)
7399
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
7400
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
7401
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
7402
+ } else if ( breachDays === 30 ) {
7403
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
7404
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
7405
+ } else if ( breachDays === 60 ) {
7406
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
7407
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
7408
+ } else {
7409
+ // For other values, add the remaining days
7410
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
7411
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
7412
+ }
7413
+
7414
+ // Generate future dates from the day after current date until end of config period
7415
+ const tomorrow = new Date( currentDateObj );
7416
+ tomorrow.setDate( tomorrow.getDate() + 1 );
7417
+ const dateIterator = new Date( tomorrow );
7418
+ while ( dateIterator <= configEndDateObj ) {
7419
+ futureDates.push( formatDate( dateIterator ) );
7420
+ dateIterator.setDate( dateIterator.getDate() + 1 );
7421
+ }
7422
+
7423
+ // Check past days within the period (from startDate to currentDate - 1);
7424
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
7425
+ const pastIterator = new Date( startDateObj );
7426
+ while ( pastIterator < currentDateObj ) {
7427
+ const dateStr = formatDate( pastIterator );
7428
+ const pastQuery = {
7429
+ query: {
7430
+ bool: {
7431
+ must: [
7432
+ { term: { 'storeId.keyword': inputData?.storeId } },
7433
+ { term: { dateString: dateStr } },
7434
+ { term: { 'type.keyword': 'store' } },
7435
+ ],
7436
+ },
7437
+ },
7438
+ };
7439
+
7440
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
7441
+ const foundHits = searchRes?.body?.hits?.hits || [];
7442
+
7443
+ if ( foundHits.length === 0 ) {
7444
+ const record = {
7445
+ clientId: getstoreName?.clientId,
7446
+ storeId: storeId,
7447
+ storeName: getstoreName?.storeName,
7448
+ dateString: dateStr,
7449
+ status: 'block',
7450
+ };
7451
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: dateStr }, record );
7452
+ }
7453
+ pastIterator.setDate( pastIterator.getDate() + 1 );
7454
+ }
7455
+
7456
+ // Insert a block record for each future date where no store record exists
7457
+ for ( const futureDateString of futureDates ) {
7458
+ const pastQuery = {
7459
+ query: {
7460
+ bool: {
7461
+ must: [
7462
+ { term: { 'storeId.keyword': storeId } },
7463
+ { term: { dateString: futureDateString } },
7464
+ { term: { 'type.keyword': 'store' } },
7465
+ ],
7466
+ },
7467
+ },
7468
+ };
7469
+
7470
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
7471
+ const foundHits = searchRes?.body?.hits?.hits || [];
7472
+
7473
+ if ( foundHits.length === 0 ) {
7474
+ const record = {
7475
+ clientId: getstoreName?.clientId,
7476
+ storeId: storeId,
7477
+ storeName: getstoreName?.storeName,
7478
+ dateString: futureDateString,
7479
+ status: 'block',
7480
+ };
7481
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
7482
+ }
7483
+ }
7484
+ }
7485
+ }
7486
+ } catch ( error ) {
7487
+ const err = error.message || 'Internal Server Error';
7488
+ logger.error( { error: error, funtion: 'ticketreview' } );
7489
+ return res.sendError( err, 500 );
7490
+ }
7491
+ }
7492
+
7493
+ // Returns footfall-directory-tagging records whose audit-input-images S3 folder
7494
+ // (v4rnd-vms-audit-input-images/${storeId}/${dateString}/) is empty or missing.
7495
+ // Query params: fromDate, toDate (YYYY-MM-DD). Defaults to Jan 1 - Apr 30 of the current year.
7496
+ // export async function getUnmatchedAuditFolders( req, res ) {
7497
+ // try {
7498
+ // const openSearch = JSON.parse( process.env.OPENSEARCH );
7499
+ // const bucketConfig = JSON.parse( process.env.BUCKET );
7500
+ // // Audit input images bucket; folder layout is ${storeId}/${dateString}/ with dateString as DD-MM-YYYY.
7501
+ // const auditInputBucket = bucketConfig.tangoReviewInput || 'v4rnd-vms-audit-input-images';
7502
+
7503
+ // // dateString is stored as YYYY-MM-DD. Default window: Jan 1 - Apr 30 of the current year.
7504
+ // const currentYear = new Date().getFullYear();
7505
+ // const fromDate = req.query.fromDate || `${currentYear}-02-02`;
7506
+ // const toDate = req.query.toDate || `${currentYear}-03-01`;
7507
+
7508
+ // // 1. Pull every record in the date range (scroll to get past the 10k window).
7509
+ // const scrollQuery = {
7510
+ // size: 1000,
7511
+ // _source: [ 'storeId', 'dateString' ],
7512
+ // query: {
7513
+ // range: { dateString: { gte: fromDate, lte: toDate } },
7514
+ // },
7515
+ // };
7516
+
7517
+ // let scrollResult = await searchOpenSearchData( openSearch.footfallDirectory, scrollQuery );
7518
+ // const totalCount = scrollResult?.body?.hits?.total?.value || 0;
7519
+ // if ( totalCount === 0 ) {
7520
+ // return res.sendError( 'No footfall-directory-tagging records found for the selected dates', 204 );
7521
+ // }
7522
+
7523
+ // let scrollId = scrollResult?.body?._scroll_id;
7524
+ // let hits = scrollResult?.body?.hits?.hits || [];
7525
+ // let allHits = [ ...hits ];
7526
+ // while ( hits.length > 0 ) {
7527
+ // const scrollResp = await scrollResponse( scrollId );
7528
+ // hits = scrollResp?.body?.hits?.hits || [];
7529
+ // scrollId = scrollResp?.body?._scroll_id;
7530
+ // allHits = [ ...allHits, ...hits ];
7531
+ // }
7532
+ // await clearScroll( scrollId );
7533
+
7534
+ // // 2. Keep only the main tagging docs (exclude *_internal_* records) and dedupe by storeId+dateString.
7535
+ // const uniqueRecords = new Map();
7536
+ // for ( const hit of allHits ) {
7537
+ // const docId = hit?._id || '';
7538
+ // if ( !docId.endsWith( '_footfall-directory-tagging' ) || docId.includes( '_internal_' ) ) {
7539
+ // continue;
7540
+ // }
7541
+ // const storeId = hit?._source?.storeId;
7542
+ // const dateString = hit?._source?.dateString;
7543
+ // if ( !storeId || !dateString ) {
7544
+ // continue;
7545
+ // }
7546
+ // uniqueRecords.set( `${storeId}_${dateString}`, { storeId, dateString } );
7547
+ // }
7548
+
7549
+ // const records = [ ...uniqueRecords.values() ];
7550
+ // logger.info( { records } );
7551
+ // // 3. For each record, check whether its audit-input-images folder exists.
7552
+ // // Images live in nested subfolders under ${storeId}/${dateString}/, so we use a recursive
7553
+ // // prefix check (checkFileExist has no delimiter) instead of listing direct children only.
7554
+ // // dateString (YYYY-MM-DD) -> folder (DD-MM-YYYY).
7555
+ // const unmatched = [];
7556
+ // const errored = [];
7557
+ // const batches = await chunkArray( records, 25 );
7558
+ // logger.info( { batches } );
7559
+ // for ( const batch of batches ) {
7560
+ // await Promise.all(
7561
+ // batch.map( async ( record ) => {
7562
+ // const folderDate = dayjs( record.dateString ).format( 'DD-MM-YYYY' );
7563
+ // const folderPath = `/${record.storeId}/${folderDate}/`;
7564
+ // const exists = await checkFileExist( {
7565
+ // Bucket: auditInputBucket,
7566
+ // Key: folderPath,
7567
+ // } );
7568
+ // logger.info( { exists, auditInputBucket, folderPath } );
7569
+ // // checkFileExist returns true/false on success, or the error object on failure.
7570
+ // if ( exists === false ) {
7571
+ // unmatched.push( { ...record, folderPath } );
7572
+ // } else if ( exists !== true ) {
7573
+ // errored.push( { ...record, folderPath } );
7574
+ // }
7575
+ // } ),
7576
+ // );
7577
+ // }
7578
+
7579
+ // return res.sendSuccess( {
7580
+ // fromDate,
7581
+ // toDate,
7582
+ // bucket: auditInputBucket,
7583
+ // totalRecords: records.length,
7584
+ // unmatchedCount: unmatched.length,
7585
+ // unmatched,
7586
+ // erroredCount: errored.length,
7587
+ // errored,
7588
+ // } );
7589
+ // } catch ( error ) {
7590
+ // const err = error.message || 'Internal Server Error';
7591
+ // logger.error( { error: error, function: 'getUnmatchedAuditFolders' } );
7592
+ // return res.sendError( err, 500 );
7593
+ // }
7594
+ // }
7595
+
7596
+