tango-app-api-infra 3.9.25-vmsbug.2 → 3.9.25

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
 
@@ -279,6 +280,7 @@ export async function tangoReviewTicket( req, res ) {
279
280
  createdByEmail: req?.user?.email,
280
281
  createdByUserName: req?.user?.userName,
281
282
  createdByRole: req?.user?.role,
283
+ contactEmail: req?.user?.email,
282
284
  createdAt: new Date(),
283
285
  },
284
286
  );
@@ -297,6 +299,7 @@ export async function tangoReviewTicket( req, res ) {
297
299
  status: parseInt( inputData?.mappingInfo?.revicedPerc || 0 ) < parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview || 0 ) ? 'Open - Accuracy Issue' : 'Closed',
298
300
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
299
301
  revicedPerc: inputData.mappingInfo?.revicedPerc,
302
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
300
303
  mappingInfo: ticketData?.[0]?._source?.mappingInfo,
301
304
  createdByEmail: req?.user?.email,
302
305
  createdByUserName: req?.user?.userName,
@@ -314,6 +317,7 @@ export async function tangoReviewTicket( req, res ) {
314
317
  const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
315
318
  // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
316
319
  const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
320
+ const accuracy = getConfig?.footfallDirectoryConfigs?.accuracyBreach || null;
317
321
  if (
318
322
  isAutoCloseEnable === true &&
319
323
  revisedPercentage >= autoCloseAccuracyValue
@@ -329,6 +333,7 @@ export async function tangoReviewTicket( req, res ) {
329
333
  mode: inputData.mappingInfo?.mode,
330
334
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
331
335
  revicedPerc: inputData.mappingInfo?.revicedPerc,
336
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
332
337
  count: inputData.mappingInfo?.count,
333
338
  revisedDetail: inputData.mappingInfo?.revisedDetail,
334
339
  status: 'Closed',
@@ -358,12 +363,14 @@ export async function tangoReviewTicket( req, res ) {
358
363
  mode: inputData.mappingInfo?.mode,
359
364
  revicedFootfall: inputData.mappingInfo?.revicedFootfall,
360
365
  revicedPerc: inputData.mappingInfo?.revicedPerc,
366
+ reviced: parseInt( inputData?.mappingInfo?.revicedPerc ),
361
367
  count: inputData.mappingInfo?.count,
362
368
  revisedDetail: inputData.mappingInfo?.revisedDetail,
363
369
  status: 'Closed',
364
370
  createdByEmail: req?.user?.email,
365
371
  createdByUserName: req?.user?.userName,
366
372
  createdByRole: req?.user?.role,
373
+ contactEmail: req?.user?.email,
367
374
  createdAt: new Date(),
368
375
  },
369
376
  );
@@ -416,6 +423,7 @@ export async function tangoReviewTicket( req, res ) {
416
423
  createdByEmail: req?.user?.email,
417
424
  createdByUserName: req?.user?.userName,
418
425
  createdByRole: req?.user?.role,
426
+ contactEmail: req?.user?.email,
419
427
  createdAt: new Date(),
420
428
  },
421
429
 
@@ -467,6 +475,7 @@ export async function tangoReviewTicket( req, res ) {
467
475
  createdByEmail: req?.user?.email,
468
476
  createdByUserName: req?.user?.userName,
469
477
  createdByRole: req?.user?.role,
478
+ contactEmail: req?.user?.email,
470
479
  createdAt: new Date(),
471
480
  },
472
481
  );
@@ -504,24 +513,310 @@ export async function tangoReviewTicket( req, res ) {
504
513
  // return;
505
514
 
506
515
  const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
507
-
508
516
  if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
509
- if ( ( record.status === 'Closed' || record.status === 'Open - Accuracy Issue' ) && inputData.ticketType !== 'internal' && tangoApproved ) {
517
+ if ( accuracy && accuracy.ticketCount && accuracy.days && tangoApproved ) {
518
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
519
+
520
+
521
+ const breachDays = Number( accuracy.days );
522
+ const breachCount = Number( accuracy.ticketCount );
523
+
524
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
525
+ // const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
526
+ const ticketName = 'footfall-directory'; // adjust if ticket name variable differs
527
+
528
+
529
+ const formatDate = ( d ) =>
530
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
531
+
532
+ // Compute current date object
533
+
534
+
535
+ // Calculate start date based on config days
536
+ // If 30 days: start from first day of current month
537
+ // If 60 days: start from first day of last month
538
+ const currentDateObj = new Date( inputData?.dateString ); // Declare currentDateObj as today's date
539
+ const startDateObj = new Date( currentDateObj );
540
+
541
+ if ( breachDays === 7 ) {
542
+ // Consider within this week (start from Monday)
543
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
544
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
545
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
546
+ } else if ( breachDays === 30 ) {
547
+ // Consider within this month
548
+ startDateObj.setDate( 1 ); // First day of current month
549
+ } else if ( breachDays === 60 ) {
550
+ // Consider this month and last month
551
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
552
+ startDateObj.setDate( 1 ); // First day of last month
553
+ } else {
554
+ // For other values, calculate months from days
555
+ const breachMonths = Math.ceil( breachDays / 30 );
556
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
557
+ startDateObj.setDate( 1 );
558
+ }
559
+
560
+
561
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
562
+ const startDate = formatDate( startDateObj );
563
+
564
+ // Calculate end date of the period (end of week/month) for dateString range
565
+ const endDateObj = new Date( currentDateObj );
566
+ if ( breachDays === 7 ) {
567
+ const dayOfWeek = endDateObj.getDay();
568
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
569
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
570
+ } else if ( breachDays === 30 ) {
571
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
572
+ } else if ( breachDays === 60 ) {
573
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
574
+ } else {
575
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
576
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
577
+ }
578
+ const endDate = formatDate( endDateObj );
579
+
580
+ // Query for tickets within this week/month window for this store and ticket name
581
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
582
+ const mappingInfoFilter = [
583
+ {
584
+ nested: {
585
+ path: 'mappingInfo',
586
+ query: {
587
+ bool: {
588
+ must: [
589
+ { term: { 'mappingInfo.type': 'approve' } },
590
+ { term: { 'mappingInfo.status': 'Closed' } },
591
+ ],
592
+ },
593
+ },
594
+ },
595
+ },
596
+ ];
597
+ if ( tangoApproved ) {
598
+ mappingInfoFilter.push( {
599
+ nested: {
600
+ path: 'mappingInfo',
601
+ query: {
602
+ term: { 'mappingInfo.type': 'tangoreview' },
603
+ },
604
+ },
605
+ } );
606
+ }
607
+ logger.info( { startDate, endDate, storeId: inputData?.storeId } );
510
608
  const query = {
511
- storeId: inputData?.storeId,
512
- isVideoStream: true,
609
+ query: {
610
+ bool: {
611
+ must: [
612
+ { term: { 'storeId.keyword': inputData?.storeId } },
613
+ { term: { 'ticketName.keyword': ticketName } },
614
+ { range: { dateString: { gte: startDate, lte: endDate } } },
615
+ ],
616
+ filter: [
617
+ {
618
+ bool: {
619
+ should: mappingInfoFilter,
620
+ minimum_should_match: 1,
621
+ },
622
+ },
623
+ ],
624
+ },
625
+ },
626
+ };
627
+ // Search in OpenSearch for recent similar tickets
628
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
629
+ logger.info( { breachTicketsResult, message: 'breachTicketsResult;;;;;;;1' } );
630
+ const getTickets = breachTicketsResult?.body?.hits?.hits || [];
631
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
632
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
633
+
634
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
635
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
636
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
637
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
638
+
639
+ const compareByOperator = ( value, operator, threshold ) => {
640
+ switch ( operator ) {
641
+ case '>': return value > threshold;
642
+ case '>=': return value >= threshold;
643
+ case '<': return value < threshold;
644
+ case '<=': return value <= threshold;
645
+ case '==': return value == threshold;
646
+ case '===': return value === threshold;
647
+ default: return value > threshold;
648
+ }
513
649
  };
514
- const getStoreType = await countDocumnetsCamera( query );
515
650
 
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 } ) )|| [];
651
+ // Filter tickets where revised footfall percentage > config accuracy
652
+ let breachTicketsCount = 0;
653
+ for ( const ticketInfo of getTickets ) {
654
+ const source = ticketInfo._source || {};
519
655
 
520
- // Prepare management eyeZone query based on storeId and dateString
656
+ const ticketMappingInfo = source.mappingInfo || [];
521
657
 
522
- const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
523
- if ( isSendMessge === true ) {
524
- logger.info( '....1' );
658
+
659
+ // Get Store/AOM accuracy (tagging stage)
660
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
661
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
662
+ const storeRevisedFF = Number( taggingEntry?.revicedFootfall )|| 0;
663
+
664
+ // Get Central Ops(tangoreview stage)
665
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
666
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
667
+ const centralOpsRevisedFF = Number( centralOpsEntry?.revicedFootfall )|| 0;
668
+
669
+ // Get Central Ops/Tango accuracy (tangoreview stage)
670
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
671
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
672
+ const tangoRevisedFF = Number( tangoEntry?.revicedFootfall )|| 0;
673
+
674
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
675
+
676
+ const accuracyDelta1 = tangoAccuracy -storeAccuracy;
677
+ const accuracyDelta2 = centralOpsAccuracy - storeAccuracy;
678
+
679
+
680
+ // Check existing breach condition OR new accuracy delta breach conditions:
681
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
682
+ // New conditions (all must be true):
683
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
684
+ // b) Post-review (Tango) accuracy > 85%
685
+ // c) Total images marked for validation > taggingBreach config
686
+ // const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
687
+ const isHighDelta = compareByOperator( accuracyDelta1, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta2, deltaBreach, deltaCountBreach );
688
+ const isCrossingThreshold =source?.reviced > gettingTangoreview;
689
+ logger.info( { source, revicedData: source?.reviced, gettingTangoreview } );
690
+ const isLowData = compareByOperator( tangoRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach ) || compareByOperator( centralOpsRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach );
691
+
692
+ const deltaBreachCondition =
693
+ isHighDelta && isCrossingThreshold && isLowData;
694
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview) || totalImages <= 10;
695
+ if ( deltaBreachCondition ) {
696
+ breachTicketsCount++;
697
+ }
698
+ }
699
+
700
+ // process.exit( 0 );
701
+ if ( breachTicketsCount > breachCount ) {
702
+ const getStoreName = await findOneStore( { storeId: inputData?.storeId }, { storeName: 1, _id: 0 } );
703
+
704
+ // Calculate remaining future days in the config period
705
+ const futureDates = [];
706
+
707
+ // Calculate end date of config period
708
+ const configEndDateObj = new Date( currentDateObj );
709
+ if ( breachDays === 7 ) {
710
+ // End of current week (Sunday)
711
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
712
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
713
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
714
+ } else if ( breachDays === 30 ) {
715
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
716
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
717
+ } else if ( breachDays === 60 ) {
718
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
719
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
720
+ } else {
721
+ // For other values, add the remaining days
722
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
723
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
724
+ }
725
+
726
+ // Generate future dates from the day after current date until end of config period
727
+ const tomorrow = new Date( currentDateObj );
728
+ tomorrow.setDate( tomorrow.getDate() + 1 );
729
+
730
+ const dateIterator = new Date( tomorrow );
731
+ while ( dateIterator <= configEndDateObj ) {
732
+ futureDates.push( formatDate( dateIterator ) );
733
+ dateIterator.setDate( dateIterator.getDate() + 1 );
734
+ }
735
+
736
+ // Check past days within the period (from startDate to currentDate - 1);
737
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
738
+ const pastIterator = new Date( startDateObj );
739
+ while ( pastIterator < currentDateObj ) {
740
+ const dateStr = formatDate( pastIterator );
741
+ const pastQuery = {
742
+ query: {
743
+ bool: {
744
+ must: [
745
+ { term: { 'storeId.keyword': inputData?.storeId } },
746
+ { term: { dateString: dateStr } },
747
+ { term: { 'type.keyword': 'store' } },
748
+ ],
749
+ },
750
+ },
751
+ };
752
+
753
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
754
+ const foundHits = searchRes?.body?.hits?.hits || [];
755
+ const getStoreName = await findOneStore( { storeId: inputData?.storeId }, { storeName: 1 } );
756
+
757
+ if ( foundHits.length === 0 ) {
758
+ const record = {
759
+ clientId: inputData?.storeId?.split( '-' )[0],
760
+ storeId: inputData?.storeId,
761
+ storeName: getStoreName?.storeName,
762
+ dateString: dateStr,
763
+ status: 'block',
764
+ };
765
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: dateStr }, record );
766
+ }
767
+ pastIterator.setDate( pastIterator.getDate() + 1 );
768
+ }
769
+
770
+ // Insert a block record for each future date where no store record exists
771
+ for ( const futureDateString of futureDates ) {
772
+ const pastQuery = {
773
+ query: {
774
+ bool: {
775
+ must: [
776
+ { term: { 'storeId.keyword': inputData?.storeId } },
777
+ { term: { dateString: futureDateString } },
778
+ { term: { 'type.keyword': 'store' } },
779
+ ],
780
+ },
781
+ },
782
+ };
783
+
784
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
785
+ const foundHits = searchRes?.body?.hits?.hits || [];
786
+
787
+ if ( foundHits.length === 0 ) {
788
+ const record = {
789
+ clientId: inputData?.storeId?.split( '-' )[0],
790
+ storeId: inputData?.storeId,
791
+ storeName: getStoreName?.storeName,
792
+ dateString: futureDateString,
793
+ status: 'block',
794
+ };
795
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
796
+ }
797
+ }
798
+ }
799
+ }
800
+ if ( isAutoCloseEnable === true && revisedPercentage >= autoCloseAccuracyValue ) {
801
+ logger.info( 'Ticket auto-closed due to meeting auto-close accuracy threshold' );
802
+ } else {
803
+ if ( ( record.status === 'Closed' || record.status === 'Open - Accuracy Issue' ) && inputData.ticketType !== 'internal' && tangoApproved ) {
804
+ const query = {
805
+ storeId: inputData?.storeId,
806
+ isVideoStream: true,
807
+ };
808
+ const getStoreType = await countDocumnetsCamera( query );
809
+
810
+ // Get all tempIds from revopInfo response
811
+ const temp = [];
812
+ inputData?.mappingInfo?.revisedDetail?.map( ( hit ) => ( hit?.revopsType === 'duplicate' && hit?.isParent === true ) ? '':temp.push( { tempId: hit?.tempId } ) )|| [];
813
+
814
+ // Prepare management eyeZone query based on storeId and dateString
815
+
816
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
817
+ if ( isSendMessge === true ) {
818
+ logger.info( '....1' );
819
+ }
525
820
  }
526
821
  }
527
822
  return res.sendSuccess( 'Ticket closed successfully' );
@@ -1615,7 +1910,7 @@ export async function ticketSummary( req, res ) {
1615
1910
  closedTickets,
1616
1911
  dueToday: dueToday,
1617
1912
  Expired: expiredTickets,
1618
- ...( isApprover !== true? underTangoReview: undertangoTickets ),
1913
+ ...( isApprover !== true? { underTangoReview: undertangoTickets }:'' ),
1619
1914
  avgTicket: ticketPercentageAvg + '%',
1620
1915
  avgAccuracy: ticketAccuracy + '%',
1621
1916
  };
@@ -2073,7 +2368,7 @@ export async function ticketSummary( req, res ) {
2073
2368
  closedTickets,
2074
2369
  dueToday: dueToday,
2075
2370
  Expired: expiredTickets,
2076
- ...( isApprover !== true? underTangoReview: undertangoTickets ),
2371
+ ...( isApprover !== true? { underTangoReview: undertangoTickets } : '' ),
2077
2372
  avgTicket: ticketPercentageAvg + '%',
2078
2373
  avgAccuracy: ticketAccuracy + '%',
2079
2374
  };
@@ -2557,7 +2852,7 @@ export async function ticketList( req, res ) {
2557
2852
  const revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
2558
2853
  if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
2559
2854
  for ( const r of revisionArray ) {
2560
- if ( r.actionType === 'approver' && r.isChecked === false ) {
2855
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
2561
2856
  isApprover = true;
2562
2857
  }
2563
2858
  }
@@ -4020,7 +4315,9 @@ export async function ticketList( req, res ) {
4020
4315
  'Ticket ID': item?.ticketId,
4021
4316
  'Store Name': item?.storeName,
4022
4317
  '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' ) : '',
4318
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4319
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4320
+ '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
4321
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4025
4322
  // 'Closed Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4026
4323
  'Due Date': ( () => {
@@ -4033,6 +4330,7 @@ export async function ticketList( req, res ) {
4033
4330
  }
4034
4331
  return '';
4035
4332
  } )(),
4333
+ 'Actual FF': item?.footfallCount,
4036
4334
  ...( function() {
4037
4335
  let counts = undefined;
4038
4336
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4060,14 +4358,15 @@ export async function ticketList( req, res ) {
4060
4358
  }
4061
4359
  return {};
4062
4360
  } )(),
4063
- 'Actual FF': item?.footfallCount,
4064
- // 'Revised FF': item?.revicedFootfall,
4361
+ 'Revised FF': item?.revicedFootfall,
4065
4362
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4066
4363
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4067
4364
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4068
4365
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4069
4366
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4070
4367
  // 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4368
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4369
+ // 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4071
4370
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4072
4371
 
4073
4372
  } );
@@ -4080,11 +4379,41 @@ export async function ticketList( req, res ) {
4080
4379
  ticketId: item?.ticketId,
4081
4380
  storeId: item?.storeId,
4082
4381
  storeName: item?.storeName,
4382
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4383
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4083
4384
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4084
4385
  issueDate: item?.dateString,
4085
4386
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4086
4387
  footfall: item?.footfallCount,
4087
- // revicedFootfall: item?.revicedFootfall,
4388
+ ...( function() {
4389
+ let counts = undefined;
4390
+ if ( Array.isArray( item.mappingInfo ) ) {
4391
+ // Priority: tangoreview > approve > review > tagging
4392
+ const targetTypes = [ 'approve', 'review', 'tagging' ];
4393
+ for ( let type of targetTypes ) {
4394
+ const found = item.mappingInfo.find( ( f ) => f && f.type === type && Array.isArray( f.count ) );
4395
+ if ( found ) {
4396
+ counts = found.count;
4397
+ break;
4398
+ }
4399
+ }
4400
+ }
4401
+ // Fallback to item.count if not found
4402
+ if ( !counts && Array.isArray( item.count ) ) {
4403
+ counts = item.count;
4404
+ }
4405
+ if ( Array.isArray( counts ) ) {
4406
+ return counts.reduce( ( acc, curr ) => {
4407
+ if ( curr && curr.name && typeof curr.value !== 'undefined' ) {
4408
+ acc[curr.name.replace( /\s/g, '' )] = curr.value;
4409
+ // acc[curr.name] = curr.value;
4410
+ }
4411
+ return acc;
4412
+ }, {} );
4413
+ }
4414
+ return {};
4415
+ } )(),
4416
+ revicedFootfall: item?.revicedFootfall,
4088
4417
  type: item.type || 'store',
4089
4418
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4090
4419
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
@@ -4092,6 +4421,8 @@ export async function ticketList( req, res ) {
4092
4421
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4093
4422
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4094
4423
  // tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4424
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4425
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4095
4426
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4096
4427
 
4097
4428
  } );
@@ -4106,9 +4437,9 @@ export async function ticketList( req, res ) {
4106
4437
  'Ticket ID': item?.ticketId,
4107
4438
  'Store Name': item?.storeName,
4108
4439
  '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' ) : '--',
4440
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4441
+ '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
4442
  '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
4443
  'Due Date': ( () => {
4113
4444
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
4114
4445
  if ( dueDate ) {
@@ -4119,6 +4450,8 @@ export async function ticketList( req, res ) {
4119
4450
  }
4120
4451
  return '';
4121
4452
  } )(),
4453
+ 'Actual FF': item?.footfallCount,
4454
+
4122
4455
  ...( function() {
4123
4456
  let counts = undefined;
4124
4457
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4146,14 +4479,18 @@ export async function ticketList( req, res ) {
4146
4479
  }
4147
4480
  return {};
4148
4481
  } )(),
4149
- 'Actual FF': item?.footfallCount,
4150
- 'Revised FF': item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.revicedFootfall : '--',
4482
+ 'Revised FF': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4151
4483
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4152
4484
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4153
- ...( isApprover !== true? 'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '' ),
4485
+ ...( isApprover !== true ?
4486
+ {
4487
+ 'Tango (Accuracy%)':
4488
+ item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4489
+ } :
4490
+ {} ),
4154
4491
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4492
+ '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
4493
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4156
- 'Created by': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4157
4494
 
4158
4495
  } );
4159
4496
  }
@@ -4167,6 +4504,7 @@ export async function ticketList( req, res ) {
4167
4504
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4168
4505
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4169
4506
  issueDate: item?.dateString,
4507
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4170
4508
  footfall: item?.footfallCount,
4171
4509
  ...( function() {
4172
4510
  let counts = undefined;
@@ -4196,15 +4534,13 @@ export async function ticketList( req, res ) {
4196
4534
  }
4197
4535
  return {};
4198
4536
  } )(),
4199
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4200
4537
  type: item.type || 'store',
4538
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4201
4539
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4202
4540
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4203
4541
  ...( isApprover !== true? item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc : '' ),
4204
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4205
-
4206
4542
  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 : '--',
4543
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4208
4544
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4209
4545
  } );
4210
4546
  }
@@ -4220,9 +4556,9 @@ export async function ticketList( req, res ) {
4220
4556
  'Ticket ID': item?.ticketId,
4221
4557
  'Store ID': item?.storeId,
4222
4558
  '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' ) : '',
4559
+ 'Ticket Created By': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4560
+ '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
4561
  '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
4562
 
4227
4563
  'Due Date': ( () => {
4228
4564
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
@@ -4234,6 +4570,8 @@ export async function ticketList( req, res ) {
4234
4570
  }
4235
4571
  return '';
4236
4572
  } )(),
4573
+ 'Actual FF': item?.footfallCount,
4574
+
4237
4575
  ...( function() {
4238
4576
  let counts = undefined;
4239
4577
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4261,14 +4599,18 @@ export async function ticketList( req, res ) {
4261
4599
  }
4262
4600
  return {};
4263
4601
  } )(),
4264
- 'Actual FF': item?.footfallCount,
4265
4602
  // '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' ) : '--',
4603
+ 'Revised FF': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4268
4604
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4269
4605
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4270
- ...( ticketsApproveFeature !== true? 'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--' ),
4606
+ ...( isApprover !== true ?
4607
+ {
4608
+ 'Tango (Accuracy%)':
4609
+ item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4610
+ } :
4611
+ {} ),
4271
4612
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4613
+ '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
4614
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4273
4615
 
4274
4616
  } );
@@ -4284,6 +4626,7 @@ export async function ticketList( req, res ) {
4284
4626
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4285
4627
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4286
4628
  issueDate: item?.dateString,
4629
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4287
4630
  footfall: item?.footfallCount,
4288
4631
  ...( function() {
4289
4632
  let counts = undefined;
@@ -4313,23 +4656,20 @@ export async function ticketList( req, res ) {
4313
4656
  }
4314
4657
  return {};
4315
4658
  } )(),
4316
- // revicedFootfall: item?.revicedFootfall,
4317
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4659
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4318
4660
  type: item.type || 'store',
4319
4661
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4320
4662
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4321
4663
  ...( isApprover !== true? tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '' ),
4322
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4323
-
4324
4664
  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 : '--',
4665
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt : '--',
4326
4666
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4327
4667
 
4328
4668
  } );
4329
4669
  }
4330
4670
  }
4331
4671
  } else if ( ticketsApproveFeature ) {
4332
- if ( inputData.isExport ) {
4672
+ if ( inputData?.isExport ) {
4333
4673
  const exportData = [];
4334
4674
  for ( let item of ticketListData ) {
4335
4675
  exportData.push( {
@@ -4337,7 +4677,9 @@ export async function ticketList( req, res ) {
4337
4677
  'Ticket ID': item?.ticketId,
4338
4678
  'Store Name': item?.storeName,
4339
4679
  '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' ) : '',
4680
+ 'Created by': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4681
+
4682
+ '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
4683
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4342
4684
  'Due Date': ( () => {
4343
4685
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate;
@@ -4349,6 +4691,8 @@ export async function ticketList( req, res ) {
4349
4691
  }
4350
4692
  return '';
4351
4693
  } )(),
4694
+ 'Actual FF': item?.footfallCount,
4695
+
4352
4696
  ...( function() {
4353
4697
  let counts = undefined;
4354
4698
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4376,14 +4720,15 @@ export async function ticketList( req, res ) {
4376
4720
  }
4377
4721
  return {};
4378
4722
  } )(),
4379
- 'Actual FF': item?.footfallCount,
4380
4723
  'Revised FF': item?.revicedFootfall,
4381
4724
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4382
4725
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4383
4726
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4384
4727
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4385
4728
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4386
- 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4729
+ // 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4730
+ 'Closed Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4731
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4387
4732
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4388
4733
 
4389
4734
  } );
@@ -4396,10 +4741,40 @@ export async function ticketList( req, res ) {
4396
4741
  ticketId: item?.ticketId,
4397
4742
  storeId: item?.storeId,
4398
4743
  storeName: item?.storeName,
4744
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4745
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4399
4746
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4400
4747
  issueDate: item?.dateString,
4401
4748
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4402
4749
  footfall: item?.footfallCount,
4750
+ ...( function() {
4751
+ let counts = undefined;
4752
+ if ( Array.isArray( item.mappingInfo ) ) {
4753
+ // Priority: tangoreview > approve > review > tagging
4754
+ const targetTypes = [ 'approve', 'review', 'tagging' ];
4755
+ for ( let type of targetTypes ) {
4756
+ const found = item.mappingInfo.find( ( f ) => f && f.type === type && Array.isArray( f.count ) );
4757
+ if ( found ) {
4758
+ counts = found.count;
4759
+ break;
4760
+ }
4761
+ }
4762
+ }
4763
+ // Fallback to item.count if not found
4764
+ if ( !counts && Array.isArray( item.count ) ) {
4765
+ counts = item.count;
4766
+ }
4767
+ if ( Array.isArray( counts ) ) {
4768
+ return counts.reduce( ( acc, curr ) => {
4769
+ if ( curr && curr.name && typeof curr.value !== 'undefined' ) {
4770
+ acc[curr.name.replace( /\s/g, '' )] = curr.value;
4771
+ // acc[curr.name] = curr.value;
4772
+ }
4773
+ return acc;
4774
+ }, {} );
4775
+ }
4776
+ return {};
4777
+ } )(),
4403
4778
  revicedFootfall: item?.revicedFootfall,
4404
4779
  type: item.type || 'store',
4405
4780
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
@@ -4407,7 +4782,9 @@ export async function ticketList( req, res ) {
4407
4782
  approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4408
4783
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4409
4784
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4410
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4785
+ // tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4786
+ closedDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' ).status === 'Closed' ? item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.updatedAt : '--',
4787
+ // ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4411
4788
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4412
4789
 
4413
4790
  } );
@@ -4723,6 +5100,27 @@ export async function getTickets( req, res ) {
4723
5100
  mappingObj.type !== 'tangoreview'
4724
5101
  ) {
4725
5102
  mappingObj.revisedDetail = processedRevopSources;
5103
+ } else if (
5104
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
5105
+ mappingObj.type === 'tangoreview' &&
5106
+ Array.isArray( mappingObj.revisedDetail )
5107
+ ) {
5108
+ const url = JSON.parse( process.env.URL );
5109
+ const OLD_CDN = url?.tangoReviewOld;
5110
+ const NEW_CDN =url?.tangoReviewNew; // bucket name : v4rnd-vms-audit-input-images
5111
+
5112
+ const replaceCdnInFilePath = ( detail ) => {
5113
+ if ( !detail || typeof detail !== 'object' ) return detail;
5114
+ if ( typeof detail.filePath === 'string' && detail.filePath.startsWith( OLD_CDN ) ) {
5115
+ detail.filePath = detail.filePath.replace( OLD_CDN, NEW_CDN );
5116
+ }
5117
+ if ( Array.isArray( detail.duplicateImage ) ) {
5118
+ detail.duplicateImage.forEach( replaceCdnInFilePath );
5119
+ }
5120
+ return detail;
5121
+ };
5122
+
5123
+ mappingObj.revisedDetail.forEach( replaceCdnInFilePath );
4726
5124
  }
4727
5125
  } );
4728
5126
  }
@@ -4743,7 +5141,14 @@ export async function getTickets( req, res ) {
4743
5141
 
4744
5142
  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
5143
  }
4746
- return res.sendSuccess( { result: finalResponse } );
5144
+
5145
+ // If tangoApproved is false, remove tangoreview entries from mappingInfo
5146
+ const clientId = finalResponse?.[0]?._source?.clientId;
5147
+
5148
+ const getConfig = clientId? await findOneClient( { clientId }, { footfallDirectoryConfigs: 1 } ): null;
5149
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
5150
+
5151
+ return res.sendSuccess( { result: finalResponse, tangoApproved: tangoApproved } );
4747
5152
  } catch ( error ) {
4748
5153
  const err = error.message || 'Internal Server Error';
4749
5154
  logger.error( { error: error, messgage: req.query } );
@@ -5075,6 +5480,29 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5075
5480
  sqsProduceQueue.MessageBody,
5076
5481
  );
5077
5482
  if ( sqsQueue.statusCode ) {
5483
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
5484
+ const logs = {
5485
+ QueueUrl: sqsProduceQueue.QueueUrl,
5486
+ MessageBody: sqsProduceQueue.MessageBody,
5487
+ sqsQueue: sqsQueue,
5488
+ statusCode: sqsQueue.statusCode,
5489
+ store_id: storeId,
5490
+ store_date: inputData.dateString.split( '-' ).reverse().join( '-' ),
5491
+ bucket_name: '',
5492
+ zone_id: 'traffic_zone',
5493
+ process_type: 'live',
5494
+ revop_type: 'footfall',
5495
+ temp_id: tempId,
5496
+ tempId1: inputData?.log?.temp1 || null,
5497
+ tempIds: inputData?.log?.tempIds || null,
5498
+ mapping: JSON.stringify( inputData?.log?.mapping ) || null,
5499
+ managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || null,
5500
+ managerEyeZoneQuery: inputData?.log?.managerEyeZoneQuery || null,
5501
+ time: Date.now(),
5502
+ createdAt: new Date(),
5503
+ createIST: dayjs().tz( 'Asia/Kolkata' ).format( 'YYYY-MM-DD' ),
5504
+ };
5505
+ await insertWithId( openSearch.revopsLog, id, logs );
5078
5506
  logger.error( {
5079
5507
  error: `${sqsQueue}`,
5080
5508
  type: 'UPLOAD_ERROR',
@@ -5093,11 +5521,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5093
5521
  process_type: 'live',
5094
5522
  revop_type: 'footfall',
5095
5523
  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 || '',
5524
+ tempId1: inputData?.log?.temp1 || null,
5525
+ tempIds: inputData?.log?.tempIds || null,
5526
+ mapping: JSON.stringify( inputData?.log?.mapping ) || null,
5527
+ managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || null,
5528
+ managerEyeZoneQuery: inputData?.log?.managerEyeZoneQuery || null,
5101
5529
  time: Date.now(),
5102
5530
  createdAt: new Date(),
5103
5531
  createIST: dayjs().tz( 'Asia/Kolkata' ).format( 'YYYY-MM-DD' ),
@@ -5865,7 +6293,8 @@ export async function updateTempStatus( req, res ) {
5865
6293
  isChecked = status === 'approved' ? true : false;
5866
6294
  break;
5867
6295
  case 'approve':
5868
- isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
6296
+ const reviewAction = actions.find( ( item ) => item.actionType === 'review' )?.action;
6297
+ isChecked = ( reviewAction === 'approved' ) === ( status === 'approved' );
5869
6298
  break;
5870
6299
  }
5871
6300
  docsToUpdate.push( {
@@ -6102,6 +6531,7 @@ export async function multiCloseTicket( req, res ) {
6102
6531
  const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
6103
6532
  const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
6104
6533
  const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
6534
+ const accuracy = getConfig?.footfallDirectoryConfigs?.accuracyBreach || null;
6105
6535
 
6106
6536
  // inputData structure should include an array of items to close
6107
6537
  // Accept both array or single ticket update
@@ -6211,6 +6641,7 @@ export async function multiCloseTicket( req, res ) {
6211
6641
  createdByEmail: req?.user?.email,
6212
6642
  createdByUserName: req?.user?.userName,
6213
6643
  createdByRole: req?.user?.role,
6644
+ contactEmail: req?.user?.email,
6214
6645
  createdAt: new Date(),
6215
6646
  updatedAt: new Date(),
6216
6647
  },
@@ -6255,6 +6686,7 @@ export async function multiCloseTicket( req, res ) {
6255
6686
  createdByEmail: req?.user?.email,
6256
6687
  createdByUserName: req?.user?.userName,
6257
6688
  createdByRole: req?.user?.role,
6689
+ contactEmail: req?.user?.email,
6258
6690
  createdAt: new Date(),
6259
6691
  updatedAt: new Date(),
6260
6692
  },
@@ -6270,7 +6702,7 @@ export async function multiCloseTicket( req, res ) {
6270
6702
  const ticketUpdatePayload = {
6271
6703
  doc: {
6272
6704
  mappingInfo: newMappingInfoArray,
6273
- status: ( revised < tangoReview && tangoApproved )? 'Under Tango Review': !tangoApproved? 'Closed': 'Approver-Closed', // status updated at top level as well
6705
+ status: ( revised < tangoReview && tangoApproved )? 'Under Tango Review': 'Closed', // status updated at top level as well
6274
6706
  updatedAt: new Date(),
6275
6707
  },
6276
6708
  };
@@ -6282,6 +6714,291 @@ export async function multiCloseTicket( req, res ) {
6282
6714
  results.push( { storeId, dateString, success: false, error: `Failed to update ticket this store ${storeId} for this date ${dateString}` } );
6283
6715
  continue;
6284
6716
  } else {
6717
+ if ( accuracy && accuracy.ticketCount && accuracy.days ) {
6718
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
6719
+
6720
+
6721
+ const breachDays = Number( accuracy.days );
6722
+ const breachCount = Number( accuracy.ticketCount );
6723
+
6724
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
6725
+ // const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
6726
+ const ticketName = 'footfall-directory'; // adjust if ticket name variable differs
6727
+
6728
+
6729
+ const formatDate = ( d ) =>
6730
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
6731
+
6732
+ // Compute current date object
6733
+
6734
+
6735
+ // Calculate start date based on config days
6736
+ // If 30 days: start from first day of current month
6737
+ // If 60 days: start from first day of last month
6738
+ const currentDateObj = new Date( dateString ); // Declare currentDateObj as today's date
6739
+ const startDateObj = new Date( currentDateObj );
6740
+
6741
+ if ( breachDays === 7 ) {
6742
+ // Consider within this week (start from Monday)
6743
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
6744
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
6745
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
6746
+ } else if ( breachDays === 30 ) {
6747
+ // Consider within this month
6748
+ startDateObj.setDate( 1 ); // First day of current month
6749
+ } else if ( breachDays === 60 ) {
6750
+ // Consider this month and last month
6751
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
6752
+ startDateObj.setDate( 1 ); // First day of last month
6753
+ } else {
6754
+ // For other values, calculate months from days
6755
+ const breachMonths = Math.ceil( breachDays / 30 );
6756
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
6757
+ startDateObj.setDate( 1 );
6758
+ }
6759
+
6760
+
6761
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
6762
+ const startDate = formatDate( startDateObj );
6763
+
6764
+ // Calculate end date of the period (end of week/month) for dateString range
6765
+ const endDateObj = new Date( currentDateObj );
6766
+ if ( breachDays === 7 ) {
6767
+ const dayOfWeek = endDateObj.getDay();
6768
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
6769
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
6770
+ } else if ( breachDays === 30 ) {
6771
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
6772
+ } else if ( breachDays === 60 ) {
6773
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
6774
+ } else {
6775
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
6776
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
6777
+ }
6778
+ const endDate = formatDate( endDateObj );
6779
+
6780
+ // Query for tickets within this week/month window for this store and ticket name
6781
+ const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
6782
+ const mappingInfoFilter = [
6783
+ {
6784
+ nested: {
6785
+ path: 'mappingInfo',
6786
+ query: {
6787
+ bool: {
6788
+ must: [
6789
+ { term: { 'mappingInfo.type': 'approve' } },
6790
+ { term: { 'mappingInfo.status': 'Closed' } },
6791
+ ],
6792
+ },
6793
+ },
6794
+ },
6795
+ },
6796
+ ];
6797
+ if ( tangoApproved ) {
6798
+ mappingInfoFilter.push( {
6799
+ nested: {
6800
+ path: 'mappingInfo',
6801
+ query: {
6802
+ term: { 'mappingInfo.type': 'tangoreview' },
6803
+ },
6804
+ },
6805
+ } );
6806
+ }
6807
+ logger.info( { startDate, endDate, storeId } );
6808
+ const query = {
6809
+ query: {
6810
+ bool: {
6811
+ must: [
6812
+ { term: { 'storeId.keyword': storeId } },
6813
+ { term: { 'ticketName.keyword': ticketName } },
6814
+ { range: { dateString: { gte: startDate, lte: endDate } } },
6815
+ ],
6816
+ filter: [
6817
+ {
6818
+ bool: {
6819
+ should: mappingInfoFilter,
6820
+ minimum_should_match: 1,
6821
+ },
6822
+ },
6823
+ ],
6824
+ },
6825
+ },
6826
+ };
6827
+ // Search in OpenSearch for recent similar tickets
6828
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
6829
+ logger.info( { breachTicketsResult, message: 'breachTicketsResult;;;;;;;1' } );
6830
+ const getTickets = breachTicketsResult?.body?.hits?.hits || [];
6831
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
6832
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
6833
+
6834
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
6835
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
6836
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
6837
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
6838
+
6839
+ const compareByOperator = ( value, operator, threshold ) => {
6840
+ switch ( operator ) {
6841
+ case '>': return value > threshold;
6842
+ case '>=': return value >= threshold;
6843
+ case '<': return value < threshold;
6844
+ case '<=': return value <= threshold;
6845
+ case '==': return value == threshold;
6846
+ case '===': return value === threshold;
6847
+ default: return value > threshold;
6848
+ }
6849
+ };
6850
+
6851
+ // Filter tickets where revised footfall percentage > config accuracy
6852
+ let breachTicketsCount = 0;
6853
+ logger.info( { getTickets, message: 'tickets;;;;;;;1' } );
6854
+ for ( const ticketInfo of getTickets ) {
6855
+ const source = ticketInfo._source || {};
6856
+
6857
+ const ticketMappingInfo = source.mappingInfo || [];
6858
+
6859
+
6860
+ // Get Store/AOM accuracy (tagging stage)
6861
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
6862
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
6863
+ const storeRevisedFF = Number( taggingEntry?.revicedFootfall )|| 0;
6864
+
6865
+ // Get Central Ops(tangoreview stage)
6866
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
6867
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
6868
+ const centralOpsRevisedFF = Number( centralOpsEntry?.revicedFootfall )|| 0;
6869
+
6870
+ // Get Central Ops/Tango accuracy (tangoreview stage)
6871
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
6872
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
6873
+ const tangoRevisedFF = Number( tangoEntry?.revicedFootfall )|| 0;
6874
+
6875
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
6876
+
6877
+ const accuracyDelta1 = tangoAccuracy -storeAccuracy;
6878
+ const accuracyDelta2 = centralOpsAccuracy - storeAccuracy;
6879
+
6880
+
6881
+ // Check existing breach condition OR new accuracy delta breach conditions:
6882
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
6883
+ // New conditions (all must be true):
6884
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
6885
+ // b) Post-review (Tango) accuracy > 85%
6886
+ // c) Total images marked for validation > taggingBreach config
6887
+ // const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
6888
+ const isHighDelta = compareByOperator( accuracyDelta1, deltaBreach, deltaCountBreach ) || compareByOperator( accuracyDelta2, deltaBreach, deltaCountBreach );
6889
+ const isCrossingThreshold =source?.reviced > gettingTangoreview;
6890
+ logger.info( { source, revicedData: source?.reviced, gettingTangoreview } );
6891
+ const isLowData = compareByOperator( tangoRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach ) || compareByOperator( centralOpsRevisedFF - storeRevisedFF, taggingBreach, taggingCountBreach );
6892
+
6893
+ const deltaBreachCondition =
6894
+ isHighDelta && isCrossingThreshold && isLowData;
6895
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview) || totalImages <= 10;
6896
+ logger.info( { isHighDelta, isCrossingThreshold, isLowData } );
6897
+ if ( deltaBreachCondition ) {
6898
+ breachTicketsCount++;
6899
+ }
6900
+ }
6901
+ logger.info( { breachTicketsCount, breachCount }, 'total Breach Tickets Count1' );
6902
+
6903
+ // process.exit( 0 );
6904
+ if ( breachTicketsCount > breachCount ) {
6905
+ const getStoreName = await findOneStore( { storeId: storeId }, { storeName: 1, _id: 0 } );
6906
+
6907
+ // Calculate remaining future days in the config period
6908
+ const futureDates = [];
6909
+
6910
+ // Calculate end date of config period
6911
+ const configEndDateObj = new Date( currentDateObj );
6912
+ if ( breachDays === 7 ) {
6913
+ // End of current week (Sunday)
6914
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
6915
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
6916
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
6917
+ } else if ( breachDays === 30 ) {
6918
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
6919
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
6920
+ } else if ( breachDays === 60 ) {
6921
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
6922
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
6923
+ } else {
6924
+ // For other values, add the remaining days
6925
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
6926
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
6927
+ }
6928
+
6929
+ // Generate future dates from the day after current date until end of config period
6930
+ const tomorrow = new Date( currentDateObj );
6931
+ tomorrow.setDate( tomorrow.getDate() + 1 );
6932
+
6933
+ const dateIterator = new Date( tomorrow );
6934
+ while ( dateIterator <= configEndDateObj ) {
6935
+ futureDates.push( formatDate( dateIterator ) );
6936
+ dateIterator.setDate( dateIterator.getDate() + 1 );
6937
+ }
6938
+
6939
+ // Check past days within the period (from startDate to currentDate - 1);
6940
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
6941
+ const pastIterator = new Date( startDateObj );
6942
+ while ( pastIterator < currentDateObj ) {
6943
+ const dateStr = formatDate( pastIterator );
6944
+ const pastQuery = {
6945
+ query: {
6946
+ bool: {
6947
+ must: [
6948
+ { term: { 'storeId.keyword': storeId } },
6949
+ { term: { dateString: dateStr } },
6950
+ { term: { 'type.keyword': 'store' } },
6951
+ ],
6952
+ },
6953
+ },
6954
+ };
6955
+
6956
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
6957
+ const foundHits = searchRes?.body?.hits?.hits || [];
6958
+
6959
+ if ( foundHits.length === 0 ) {
6960
+ const record = {
6961
+ clientId: inputData?.ticketList?.[0]?.storeId?.split( '-' )[0],
6962
+ storeId: storeId,
6963
+ storeName: getStoreName?.storeName,
6964
+ dateString: dateStr,
6965
+ status: 'block',
6966
+ };
6967
+ await updateOneUpsertVmsStoreRequest( { storeId: storeId, dateString: dateStr }, record );
6968
+ }
6969
+ pastIterator.setDate( pastIterator.getDate() + 1 );
6970
+ }
6971
+
6972
+ // Insert a block record for each future date where no store record exists
6973
+ for ( const futureDateString of futureDates ) {
6974
+ const pastQuery = {
6975
+ query: {
6976
+ bool: {
6977
+ must: [
6978
+ { term: { 'storeId.keyword': storeId } },
6979
+ { term: { dateString: futureDateString } },
6980
+ { term: { 'type.keyword': 'store' } },
6981
+ ],
6982
+ },
6983
+ },
6984
+ };
6985
+
6986
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
6987
+ const foundHits = searchRes?.body?.hits?.hits || [];
6988
+
6989
+ if ( foundHits.length === 0 ) {
6990
+ const record = {
6991
+ clientId: inputData?.ticketList?.[0]?.storeId?.split( '-' )[0],
6992
+ storeId: storeId,
6993
+ storeName: getStoreName?.storeName,
6994
+ dateString: futureDateString,
6995
+ status: 'block',
6996
+ };
6997
+ await updateOneUpsertVmsStoreRequest( { storeId: storeId, dateString: futureDateString }, record );
6998
+ }
6999
+ }
7000
+ }
7001
+ }
6285
7002
  const query = {
6286
7003
  storeId: storeId,
6287
7004
  isVideoStream: true,
@@ -6512,3 +7229,273 @@ export async function updateAccuracyIssues( req, res ) {
6512
7229
  return res.sendError( err, 500 );
6513
7230
  }
6514
7231
  }
7232
+
7233
+ export async function getBadTicket( req, res ) {
7234
+ try {
7235
+ if ( accuracy && accuracy.ticketCount && accuracy.days && accuracy.accuracy ) {
7236
+ // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
7237
+
7238
+ const breachDays = Number( accuracy.days );
7239
+ const breachCount = Number( accuracy.ticketCount );
7240
+
7241
+ // accuracy.accuracy is a string like "95%", so remove "%" and convert to Number
7242
+ const breachAccuracy = Number( accuracy.accuracy.replace( '%', '' ) );
7243
+ const storeId = inputData.storeId;
7244
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
7245
+
7246
+
7247
+ const formatDate = ( d ) =>
7248
+ `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
7249
+
7250
+ // Compute current date object
7251
+
7252
+
7253
+ // Calculate start date based on config days
7254
+ // If 30 days: start from first day of current month
7255
+ // If 60 days: start from first day of last month
7256
+ const currentDateObj = new Date( inputData?.dateString ); // Declare currentDateObj as today's date
7257
+ const startDateObj = new Date( currentDateObj );
7258
+
7259
+ if ( breachDays === 7 ) {
7260
+ // Consider within this week (start from Monday)
7261
+ const dayOfWeek = startDateObj.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
7262
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
7263
+ startDateObj.setDate( startDateObj.getDate() - daysFromMonday );
7264
+ } else if ( breachDays === 30 ) {
7265
+ // Consider within this month
7266
+ startDateObj.setDate( 1 ); // First day of current month
7267
+ } else if ( breachDays === 60 ) {
7268
+ // Consider this month and last month
7269
+ startDateObj.setMonth( startDateObj.getMonth() - 1 );
7270
+ startDateObj.setDate( 1 ); // First day of last month
7271
+ } else {
7272
+ // For other values, calculate months from days
7273
+ const breachMonths = Math.ceil( breachDays / 30 );
7274
+ startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
7275
+ startDateObj.setDate( 1 );
7276
+ }
7277
+
7278
+
7279
+ startDateObj.setUTCHours( 0, 0, 0, 0 );
7280
+ const startDate = formatDate( startDateObj );
7281
+
7282
+ // Calculate end date of the period (end of week/month) for dateString range
7283
+ const endDateObj = new Date( currentDateObj );
7284
+ if ( breachDays === 7 ) {
7285
+ const dayOfWeek = endDateObj.getDay();
7286
+ const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
7287
+ endDateObj.setDate( endDateObj.getDate() + daysUntilSunday );
7288
+ } else if ( breachDays === 30 ) {
7289
+ endDateObj.setMonth( endDateObj.getMonth() + 1, 0 );
7290
+ } else if ( breachDays === 60 ) {
7291
+ endDateObj.setMonth( endDateObj.getMonth() + 2, 0 );
7292
+ } else {
7293
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
7294
+ endDateObj.setDate( endDateObj.getDate() + remainingDays );
7295
+ }
7296
+ const endDate = formatDate( endDateObj );
7297
+
7298
+ // Query for tickets within this week/month window for this store and ticket name
7299
+ const query = {
7300
+ query: {
7301
+ bool: {
7302
+ must: [
7303
+ { term: { 'storeId.keyword': storeId } },
7304
+ { term: { 'ticketName.keyword': ticketName } },
7305
+ { range: { dateString: { gte: startDate, lte: endDate } } },
7306
+ ],
7307
+ must_not: [
7308
+ {
7309
+ terms: { 'status.keyword': [ 'Auto-Closed', 'Reviewer-Expired' ] },
7310
+ },
7311
+ ],
7312
+ },
7313
+ },
7314
+ };
7315
+
7316
+ // Search in OpenSearch for recent similar tickets
7317
+ const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
7318
+ logger.info( { breachTicketsResult } );
7319
+ const tickets = breachTicketsResult?.body?.hits?.hits || [];
7320
+
7321
+ const tangoTaggingDuDate = getConfig?.footfallDirectoryConfigs?.tangoReview || 0;
7322
+ const gettingTangoreview = Number( tangoTaggingDuDate.split( '%' )[0] || 0 );
7323
+
7324
+ const deltaBreach = getConfig?.footfallDirectoryConfigs?.deltaBreach || '>';
7325
+ const deltaCountBreach = Number( getConfig?.footfallDirectoryConfigs?.deltaCountBreach ?? 10 );
7326
+ const taggingBreach = getConfig?.footfallDirectoryConfigs?.taggingBreach || '>=';
7327
+ const taggingCountBreach = Number( getConfig?.footfallDirectoryConfigs?.taggingCountBreach ?? 10 );
7328
+
7329
+ const compareByOperator = ( value, operator, threshold ) => {
7330
+ switch ( operator ) {
7331
+ case '>': return value > threshold;
7332
+ case '>=': return value >= threshold;
7333
+ case '<': return value < threshold;
7334
+ case '<=': return value <= threshold;
7335
+ case '==': return value == threshold;
7336
+ case '===': return value === threshold;
7337
+ default: return value > threshold;
7338
+ }
7339
+ };
7340
+
7341
+ // Filter tickets where revised footfall percentage > config accuracy
7342
+
7343
+ let breachTicketsCount = 0;
7344
+ for ( const ticket of tickets ) {
7345
+ const source = ticket._source || {};
7346
+
7347
+ const ticketFootfallCount = source.footfallCount || 0;
7348
+ const status = source.status || '';
7349
+ const ticketMappingInfo = source.mappingInfo || [];
7350
+ // Calculate revised footfall percentage
7351
+ const ticketRevisedPercentage = source.reviced;
7352
+ // Get Store/AOM accuracy (tagging stage)
7353
+ const taggingEntry = ticketMappingInfo.find( ( m ) => m.type === 'tagging' );
7354
+ const storeAccuracy = Number( taggingEntry?.reviced ) || 0;
7355
+ // Get AOM accuracy (tagging stage)
7356
+ const reviewEntry = ticketMappingInfo.find( ( m ) => m.type === 'review' );
7357
+ const AOMAccuracy = Number( reviewEntry?.reviced ) || 0;
7358
+
7359
+ // Get Central Ops(tangoreview stage)
7360
+ const centralOpsEntry = ticketMappingInfo.find( ( m ) => m.type === 'approve' );
7361
+ const centralOpsAccuracy = Number( centralOpsEntry?.reviced ) || 0;
7362
+
7363
+ // Get Central Ops/Tango accuracy (tangoreview stage)
7364
+ const tangoEntry = ticketMappingInfo.find( ( m ) => m.type === 'tangoreview' );
7365
+ const tangoAccuracy = Number( tangoEntry?.reviced ) || 0;
7366
+
7367
+ // Calculate delta between Store/AOM and Central Ops/Tango accuracy
7368
+ const accuracyDelta1 = AOMAccuracy - storeAccuracy;
7369
+ const accuracyDelta2 = tangoAccuracy - centralOpsAccuracy;
7370
+ const accuracyDelta3 = tangoAccuracy -storeAccuracy;
7371
+ const accuracyDelta4 = centralOpsAccuracy - AOMAccuracy;
7372
+ const accuracyDelta5 = tangoAccuracy -AOMAccuracy;
7373
+ const accuracyDelta6 = centralOpsAccuracy - storeAccuracy;
7374
+ // Get total images (footfall) marked for validation
7375
+ const totalImages =ticketFootfallCount || 0;
7376
+
7377
+ // Check existing breach condition OR new accuracy delta breach conditions:
7378
+ // Existing: revised footfall percentage > config accuracy and not Auto-Closed
7379
+ // New conditions (all must be true):
7380
+ // a) Delta between Store/AOM and Central Ops/Tango accuracy > deltaBreach config
7381
+ // b) Post-review (Tango) accuracy > 85%
7382
+ // c) Total images marked for validation > taggingBreach config
7383
+ const existingBreach = ticketRevisedPercentage > breachAccuracy && status !== 'Auto-Closed';
7384
+ // const deltaBreachCondition = accuracyDelta > 10 || ( storeAccuracy < gettingTangoreview && tangoAccuracy > gettingTangoreview )|| totalImages <= 10;
7385
+ 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 );
7386
+
7387
+ const isCrossingThreshold =
7388
+ storeAccuracy < gettingTangoreview &&(
7389
+ tangoAccuracy > gettingTangoreview || centralOpsAccuracy > gettingTangoreview || AOMAccuracy > gettingTangoreview );
7390
+ const isLowData = compareByOperator( totalImages, taggingBreach, taggingCountBreach );
7391
+
7392
+ const deltaBreachCondition =
7393
+ isHighDelta || isCrossingThreshold || isLowData;
7394
+ if ( existingBreach || deltaBreachCondition ) {
7395
+ breachTicketsCount++;
7396
+ }
7397
+ }
7398
+ logger.info( { breachTicketsCount, breachCount } );
7399
+ if ( breachTicketsCount > breachCount ) {
7400
+ // Calculate remaining future days in the config period
7401
+ const futureDates = [];
7402
+
7403
+ // Calculate end date of config period
7404
+ const configEndDateObj = new Date( currentDateObj );
7405
+ if ( breachDays === 7 ) {
7406
+ // End of current week (Sunday)
7407
+ const dayOfWeek = configEndDateObj.getDay(); // 0=Sun, 6=Sat
7408
+ const daysUntilEndOfWeek = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
7409
+ configEndDateObj.setDate( configEndDateObj.getDate() + daysUntilEndOfWeek );
7410
+ } else if ( breachDays === 30 ) {
7411
+ // End of current month (use 2-arg setMonth to avoid month rollover on dates like 31st)
7412
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 1, 0 ); // Last day of current month
7413
+ } else if ( breachDays === 60 ) {
7414
+ // End of next month (use 2-arg setMonth to avoid month rollover on dates like 31st)
7415
+ configEndDateObj.setMonth( configEndDateObj.getMonth() + 2, 0 ); // Last day of next month
7416
+ } else {
7417
+ // For other values, add the remaining days
7418
+ const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
7419
+ configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
7420
+ }
7421
+
7422
+ // Generate future dates from the day after current date until end of config period
7423
+ const tomorrow = new Date( currentDateObj );
7424
+ tomorrow.setDate( tomorrow.getDate() + 1 );
7425
+ const dateIterator = new Date( tomorrow );
7426
+ while ( dateIterator <= configEndDateObj ) {
7427
+ futureDates.push( formatDate( dateIterator ) );
7428
+ dateIterator.setDate( dateIterator.getDate() + 1 );
7429
+ }
7430
+
7431
+ // Check past days within the period (from startDate to currentDate - 1);
7432
+ // if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day
7433
+ const pastIterator = new Date( startDateObj );
7434
+ while ( pastIterator < currentDateObj ) {
7435
+ const dateStr = formatDate( pastIterator );
7436
+ const pastQuery = {
7437
+ query: {
7438
+ bool: {
7439
+ must: [
7440
+ { term: { 'storeId.keyword': inputData?.storeId } },
7441
+ { term: { dateString: dateStr } },
7442
+ { term: { 'type.keyword': 'store' } },
7443
+ ],
7444
+ },
7445
+ },
7446
+ };
7447
+
7448
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
7449
+ const foundHits = searchRes?.body?.hits?.hits || [];
7450
+
7451
+ if ( foundHits.length === 0 ) {
7452
+ const record = {
7453
+ clientId: getstoreName?.clientId,
7454
+ storeId: storeId,
7455
+ storeName: getstoreName?.storeName,
7456
+ dateString: dateStr,
7457
+ status: 'block',
7458
+ };
7459
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: dateStr }, record );
7460
+ }
7461
+ pastIterator.setDate( pastIterator.getDate() + 1 );
7462
+ }
7463
+
7464
+ // Insert a block record for each future date where no store record exists
7465
+ for ( const futureDateString of futureDates ) {
7466
+ const pastQuery = {
7467
+ query: {
7468
+ bool: {
7469
+ must: [
7470
+ { term: { 'storeId.keyword': storeId } },
7471
+ { term: { dateString: futureDateString } },
7472
+ { term: { 'type.keyword': 'store' } },
7473
+ ],
7474
+ },
7475
+ },
7476
+ };
7477
+
7478
+ const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
7479
+ const foundHits = searchRes?.body?.hits?.hits || [];
7480
+
7481
+ if ( foundHits.length === 0 ) {
7482
+ const record = {
7483
+ clientId: getstoreName?.clientId,
7484
+ storeId: storeId,
7485
+ storeName: getstoreName?.storeName,
7486
+ dateString: futureDateString,
7487
+ status: 'block',
7488
+ };
7489
+ await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
7490
+ }
7491
+ }
7492
+ }
7493
+ }
7494
+ } catch ( error ) {
7495
+ const err = error.message || 'Internal Server Error';
7496
+ logger.error( { error: error, funtion: 'ticketreview' } );
7497
+ return res.sendError( err, 500 );
7498
+ }
7499
+ }
7500
+
7501
+