tango-app-api-infra 3.9.25-vmsbug.1 → 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,18 +2852,18 @@ 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
  }
2564
2859
  }
2565
2860
  const searchQuery = {
2566
2861
 
2567
- size: limit, // or use parseInt(req.query.limit) for dynamic
2568
- from: offset, // or use parseInt(req.query.offset) for dynamic
2569
- sort: [ { 'createdAt': { order: 'desc' } } ],
2570
-
2571
- query: {
2862
+ 'size': limit, // or use parseInt(req.query.limit) for dynamic
2863
+ 'from': offset, // or use parseInt(req.query.offset) for dynamic
2864
+ 'sort': [ { 'createdAt': { order: 'desc' } } ],
2865
+ 'track_total_hits': true,
2866
+ 'query': {
2572
2867
  bool: {
2573
2868
  must: [
2574
2869
  {
@@ -2817,52 +3112,6 @@ export async function ticketList( req, res ) {
2817
3112
  },
2818
3113
  ];
2819
3114
  break;
2820
- // case 'revicedPerc':
2821
- // searchQuery.sort = [
2822
- // {
2823
- // _script: {
2824
- // type: 'string',
2825
- // script: {
2826
- // lang: 'painless',
2827
- // source: `
2828
- // try {
2829
- // if (params._source != null && params._source.mappingInfo != null) {
2830
- // String found = null;
2831
-
2832
- // for (def item : params._source.mappingInfo) {
2833
- // if (item != null && item.type == params.miType && item.revicedPerc != null) {
2834
- // String value = item.revicedPerc.toString();
2835
-
2836
- // if (found == null || value.compareTo(found) > 0) {
2837
- // found = value;
2838
- // }
2839
- // }
2840
- // }
2841
-
2842
- // if (found != null) {
2843
- // return found;
2844
- // }
2845
- // }
2846
- // } catch (Exception e) {
2847
- // // ignore and fallback
2848
- // }
2849
-
2850
- // // fallback to root-level revicedPerc
2851
- // if (params._source != null && params._source.revicedPerc != null) {
2852
- // return params._source.revicedPerc.toString();
2853
- // }
2854
-
2855
- // return "";
2856
- // `,
2857
- // params: {
2858
- // miType: requestedSortType,
2859
- // },
2860
- // },
2861
- // order: sortOrder,
2862
- // },
2863
- // },
2864
- // ];
2865
- // break;
2866
3115
  case 'revicedPerc':
2867
3116
  searchQuery.sort = [
2868
3117
  {
@@ -4066,7 +4315,9 @@ export async function ticketList( req, res ) {
4066
4315
  'Ticket ID': item?.ticketId,
4067
4316
  'Store Name': item?.storeName,
4068
4317
  'Store ID': item?.storeId,
4069
- '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' ) : '',
4070
4321
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4071
4322
  // 'Closed Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4072
4323
  'Due Date': ( () => {
@@ -4079,6 +4330,7 @@ export async function ticketList( req, res ) {
4079
4330
  }
4080
4331
  return '';
4081
4332
  } )(),
4333
+ 'Actual FF': item?.footfallCount,
4082
4334
  ...( function() {
4083
4335
  let counts = undefined;
4084
4336
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4106,14 +4358,15 @@ export async function ticketList( req, res ) {
4106
4358
  }
4107
4359
  return {};
4108
4360
  } )(),
4109
- 'Actual FF': item?.footfallCount,
4110
- // 'Revised FF': item?.revicedFootfall,
4361
+ 'Revised FF': item?.revicedFootfall,
4111
4362
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4112
4363
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4113
4364
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4114
4365
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4115
4366
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4116
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 || '--',
4117
4370
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4118
4371
 
4119
4372
  } );
@@ -4126,11 +4379,41 @@ export async function ticketList( req, res ) {
4126
4379
  ticketId: item?.ticketId,
4127
4380
  storeId: item?.storeId,
4128
4381
  storeName: item?.storeName,
4382
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4383
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4129
4384
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4130
4385
  issueDate: item?.dateString,
4131
4386
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4132
4387
  footfall: item?.footfallCount,
4133
- // 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,
4134
4417
  type: item.type || 'store',
4135
4418
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4136
4419
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
@@ -4138,6 +4421,8 @@ export async function ticketList( req, res ) {
4138
4421
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4139
4422
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4140
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 || '--',
4141
4426
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4142
4427
 
4143
4428
  } );
@@ -4152,9 +4437,9 @@ export async function ticketList( req, res ) {
4152
4437
  'Ticket ID': item?.ticketId,
4153
4438
  'Store Name': item?.storeName,
4154
4439
  'Store ID': item?.storeId,
4155
- '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' ) : '--',
4156
4442
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4157
- 'Ticket Closed Date': item.status === 'Closed' || item.status === 'Tango Review Done' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4158
4443
  'Due Date': ( () => {
4159
4444
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
4160
4445
  if ( dueDate ) {
@@ -4165,6 +4450,8 @@ export async function ticketList( req, res ) {
4165
4450
  }
4166
4451
  return '';
4167
4452
  } )(),
4453
+ 'Actual FF': item?.footfallCount,
4454
+
4168
4455
  ...( function() {
4169
4456
  let counts = undefined;
4170
4457
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4192,14 +4479,18 @@ export async function ticketList( req, res ) {
4192
4479
  }
4193
4480
  return {};
4194
4481
  } )(),
4195
- 'Actual FF': item?.footfallCount,
4196
- 'Revised FF': item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.revicedFootfall : '--',
4482
+ 'Revised FF': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4197
4483
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4198
4484
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4199
- ...( 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
+ {} ),
4200
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' ) : '--',
4201
4493
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4202
- 'Created by': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4203
4494
 
4204
4495
  } );
4205
4496
  }
@@ -4213,6 +4504,7 @@ export async function ticketList( req, res ) {
4213
4504
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4214
4505
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4215
4506
  issueDate: item?.dateString,
4507
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4216
4508
  footfall: item?.footfallCount,
4217
4509
  ...( function() {
4218
4510
  let counts = undefined;
@@ -4242,15 +4534,13 @@ export async function ticketList( req, res ) {
4242
4534
  }
4243
4535
  return {};
4244
4536
  } )(),
4245
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4246
4537
  type: item.type || 'store',
4538
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4247
4539
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4248
4540
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4249
4541
  ...( isApprover !== true? item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc : '' ),
4250
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4251
-
4252
4542
  status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4253
- 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 : '--',
4254
4544
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4255
4545
  } );
4256
4546
  }
@@ -4266,9 +4556,9 @@ export async function ticketList( req, res ) {
4266
4556
  'Ticket ID': item?.ticketId,
4267
4557
  'Store ID': item?.storeId,
4268
4558
  'Store Name': item?.storeName,
4269
- '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' ) : '',
4270
4561
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4271
- 'Ticket Closed Date': item.status === 'Closed' || item.status === 'Tango Review Done' ? dayjs( item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.updatedAt ).format( 'DD MMM, YYYY' ) : '--',
4272
4562
 
4273
4563
  'Due Date': ( () => {
4274
4564
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
@@ -4280,6 +4570,8 @@ export async function ticketList( req, res ) {
4280
4570
  }
4281
4571
  return '';
4282
4572
  } )(),
4573
+ 'Actual FF': item?.footfallCount,
4574
+
4283
4575
  ...( function() {
4284
4576
  let counts = undefined;
4285
4577
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4307,14 +4599,18 @@ export async function ticketList( req, res ) {
4307
4599
  }
4308
4600
  return {};
4309
4601
  } )(),
4310
- 'Actual FF': item?.footfallCount,
4311
4602
  // 'Revised FF': item?.revicedFootfall,
4312
- 'Revised FF': item.status === 'Closed' || item.status === 'Tango Review Done' ? item?.revicedFootfall : '--',
4313
- '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 || '--',
4314
4604
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4315
4605
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4316
- ...( 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
+ {} ),
4317
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' ) : '--',
4318
4614
  'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4319
4615
 
4320
4616
  } );
@@ -4330,6 +4626,7 @@ export async function ticketList( req, res ) {
4330
4626
  ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4331
4627
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
4332
4628
  issueDate: item?.dateString,
4629
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4333
4630
  footfall: item?.footfallCount,
4334
4631
  ...( function() {
4335
4632
  let counts = undefined;
@@ -4359,23 +4656,20 @@ export async function ticketList( req, res ) {
4359
4656
  }
4360
4657
  return {};
4361
4658
  } )(),
4362
- // revicedFootfall: item?.revicedFootfall,
4363
- dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
4659
+ revisedFF: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedFootfall || '--',
4364
4660
  type: item.type || 'store',
4365
4661
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4366
4662
  reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4367
4663
  ...( isApprover !== true? tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '' ),
4368
- revisedFF: item.status === 'Closed' || item.status === 'Tango Review Done' ?item?.revicedFootfall : '--',
4369
-
4370
4664
  status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
4371
- 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 : '--',
4372
4666
  ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4373
4667
 
4374
4668
  } );
4375
4669
  }
4376
4670
  }
4377
4671
  } else if ( ticketsApproveFeature ) {
4378
- if ( inputData.isExport ) {
4672
+ if ( inputData?.isExport ) {
4379
4673
  const exportData = [];
4380
4674
  for ( let item of ticketListData ) {
4381
4675
  exportData.push( {
@@ -4383,7 +4677,9 @@ export async function ticketList( req, res ) {
4383
4677
  'Ticket ID': item?.ticketId,
4384
4678
  'Store Name': item?.storeName,
4385
4679
  'Store ID': item?.storeId,
4386
- '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' ) : '',
4387
4683
  'Issue Date': item?.dateString ? dayjs( item.dateString ).format( 'DD MMM, YYYY' ) : '',
4388
4684
  'Due Date': ( () => {
4389
4685
  const dueDate = item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate;
@@ -4395,6 +4691,8 @@ export async function ticketList( req, res ) {
4395
4691
  }
4396
4692
  return '';
4397
4693
  } )(),
4694
+ 'Actual FF': item?.footfallCount,
4695
+
4398
4696
  ...( function() {
4399
4697
  let counts = undefined;
4400
4698
  if ( Array.isArray( item.mappingInfo ) ) {
@@ -4422,14 +4720,15 @@ export async function ticketList( req, res ) {
4422
4720
  }
4423
4721
  return {};
4424
4722
  } )(),
4425
- 'Actual FF': item?.footfallCount,
4426
4723
  'Revised FF': item?.revicedFootfall,
4427
4724
  'Store (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
4428
4725
  'Reviewer (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
4429
4726
  'Approver (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4430
4727
  'Tango (Accuracy%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4431
4728
  'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4432
- '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 || '--',
4433
4732
  'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4434
4733
 
4435
4734
  } );
@@ -4442,10 +4741,40 @@ export async function ticketList( req, res ) {
4442
4741
  ticketId: item?.ticketId,
4443
4742
  storeId: item?.storeId,
4444
4743
  storeName: item?.storeName,
4744
+ ticketCreatedBy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdByEmail || '--',
4745
+ // ticketReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
4445
4746
  ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
4446
4747
  issueDate: item?.dateString,
4447
4748
  dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
4448
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
+ } )(),
4449
4778
  revicedFootfall: item?.revicedFootfall,
4450
4779
  type: item.type || 'store',
4451
4780
  storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
@@ -4453,7 +4782,9 @@ export async function ticketList( req, res ) {
4453
4782
  approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
4454
4783
  tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
4455
4784
  status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
4456
- 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 || '--',
4457
4788
  approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
4458
4789
 
4459
4790
  } );
@@ -4769,6 +5100,27 @@ export async function getTickets( req, res ) {
4769
5100
  mappingObj.type !== 'tangoreview'
4770
5101
  ) {
4771
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 );
4772
5124
  }
4773
5125
  } );
4774
5126
  }
@@ -4789,7 +5141,14 @@ export async function getTickets( req, res ) {
4789
5141
 
4790
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 );
4791
5143
  }
4792
- 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 } );
4793
5152
  } catch ( error ) {
4794
5153
  const err = error.message || 'Internal Server Error';
4795
5154
  logger.error( { error: error, messgage: req.query } );
@@ -5121,6 +5480,29 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5121
5480
  sqsProduceQueue.MessageBody,
5122
5481
  );
5123
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 );
5124
5506
  logger.error( {
5125
5507
  error: `${sqsQueue}`,
5126
5508
  type: 'UPLOAD_ERROR',
@@ -5139,11 +5521,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
5139
5521
  process_type: 'live',
5140
5522
  revop_type: 'footfall',
5141
5523
  temp_id: tempId,
5142
- tempId1: inputData?.log?.temp1 || '',
5143
- tempIds: inputData?.log?.tempIds || '',
5144
- mapping: JSON.stringify( inputData?.log?.mapping ) || '',
5145
- managerEyeZoneHit: inputData?.log?.managerEyeZoneHit || '',
5146
- 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,
5147
5529
  time: Date.now(),
5148
5530
  createdAt: new Date(),
5149
5531
  createIST: dayjs().tz( 'Asia/Kolkata' ).format( 'YYYY-MM-DD' ),
@@ -5911,7 +6293,8 @@ export async function updateTempStatus( req, res ) {
5911
6293
  isChecked = status === 'approved' ? true : false;
5912
6294
  break;
5913
6295
  case 'approve':
5914
- 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' );
5915
6298
  break;
5916
6299
  }
5917
6300
  docsToUpdate.push( {
@@ -6148,6 +6531,7 @@ export async function multiCloseTicket( req, res ) {
6148
6531
  const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
6149
6532
  const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
6150
6533
  const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
6534
+ const accuracy = getConfig?.footfallDirectoryConfigs?.accuracyBreach || null;
6151
6535
 
6152
6536
  // inputData structure should include an array of items to close
6153
6537
  // Accept both array or single ticket update
@@ -6257,6 +6641,7 @@ export async function multiCloseTicket( req, res ) {
6257
6641
  createdByEmail: req?.user?.email,
6258
6642
  createdByUserName: req?.user?.userName,
6259
6643
  createdByRole: req?.user?.role,
6644
+ contactEmail: req?.user?.email,
6260
6645
  createdAt: new Date(),
6261
6646
  updatedAt: new Date(),
6262
6647
  },
@@ -6301,6 +6686,7 @@ export async function multiCloseTicket( req, res ) {
6301
6686
  createdByEmail: req?.user?.email,
6302
6687
  createdByUserName: req?.user?.userName,
6303
6688
  createdByRole: req?.user?.role,
6689
+ contactEmail: req?.user?.email,
6304
6690
  createdAt: new Date(),
6305
6691
  updatedAt: new Date(),
6306
6692
  },
@@ -6316,7 +6702,7 @@ export async function multiCloseTicket( req, res ) {
6316
6702
  const ticketUpdatePayload = {
6317
6703
  doc: {
6318
6704
  mappingInfo: newMappingInfoArray,
6319
- 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
6320
6706
  updatedAt: new Date(),
6321
6707
  },
6322
6708
  };
@@ -6328,6 +6714,291 @@ export async function multiCloseTicket( req, res ) {
6328
6714
  results.push( { storeId, dateString, success: false, error: `Failed to update ticket this store ${storeId} for this date ${dateString}` } );
6329
6715
  continue;
6330
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
+ }
6331
7002
  const query = {
6332
7003
  storeId: storeId,
6333
7004
  isVideoStream: true,
@@ -6558,3 +7229,273 @@ export async function updateAccuracyIssues( req, res ) {
6558
7229
  return res.sendError( err, 500 );
6559
7230
  }
6560
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
+