tango-app-api-infra 3.9.5-vms.8 → 3.9.5-vms.81

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.
@@ -1,9 +1,18 @@
1
- import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger } from 'tango-app-api-middleware';
1
+ import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger, sendMessageToFIFOQueue, updateOpenSearchData } from 'tango-app-api-middleware';
2
2
  import { aggregateCluster } from '../services/cluster.service.js';
3
3
  import { findOneRevopDownload } from '../services/revopDownload.service.js';
4
4
  import { findOneStore } from '../services/store.service.js';
5
5
  import { findOneClient } from '../services/client.service.js';
6
6
  import { updateOneUpsertVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
7
+ import { findOneUser, aggregateUser } from '../services/user.service.js';
8
+ import { findteams } from '../services/teams.service.js';
9
+ import { findcluster } from '../services/cluster.service.js';
10
+ import { sendPushNotification } from 'tango-app-api-middleware';
11
+ import dayjs from 'dayjs';
12
+ import { sendSqsMessage } from '../controllers/footfallDirectory.controllers.js';
13
+ import { countDocumnetsCamera } from '../services/camera.service.js';
14
+ // import utc from 'dayjs/plugin/utc.js';
15
+ // import timezone from 'dayjs/plugin/timezone.js';
7
16
 
8
17
  function formatRevopTaggingHits( hits = [] ) {
9
18
  return hits
@@ -14,19 +23,21 @@ function formatRevopTaggingHits( hits = [] ) {
14
23
  }
15
24
 
16
25
  const duplicateImages = Array.isArray( source.duplicateImage ) ?
17
- source.duplicateImage.map( ( item ) => ( {
18
- tempId: item?.tempId,
19
- timeRange: item?.timeRange,
20
- entryTime: item?.entryTime,
21
- exitTime: item?.exitTime,
22
- filePath: item?.filePath,
23
- status: item?.status,
24
- isChecked: Boolean( item?.isChecked ),
25
- } ) ) :
26
- [];
26
+ source.duplicateImage.map( ( item ) => ( {
27
+ id: item?.id,
28
+ tempId: item?.tempId,
29
+ timeRange: item?.timeRange,
30
+ entryTime: item?.entryTime,
31
+ exitTime: item?.exitTime,
32
+ filePath: item?.filePath,
33
+ status: item?.status,
34
+ action: item?.action,
35
+ isChecked: Boolean( item?.isChecked ),
36
+ } ) ) :
37
+ [];
27
38
 
28
39
  return {
29
- id: hit?._id,
40
+ id: source?.id,
30
41
  clientId: source?.clientId,
31
42
  storeId: source?.storeId,
32
43
  tempId: source?.tempId,
@@ -41,11 +52,12 @@ function formatRevopTaggingHits( hits = [] ) {
41
52
  description: source?.description || '',
42
53
  isChecked: Boolean( source?.isChecked ),
43
54
  type: source?.type,
55
+ action: source?.action,
44
56
  parent: source?.parent ?? null,
45
57
  isParent: duplicateImages.length > 0 && !source?.parent,
46
58
  createdAt: source?.createdAt,
47
59
  updatedAt: source?.updatedAt,
48
- data: duplicateImages,
60
+ duplicateImage: duplicateImages,
49
61
  };
50
62
  } )
51
63
  .filter( Boolean );
@@ -53,7 +65,7 @@ function formatRevopTaggingHits( hits = [] ) {
53
65
 
54
66
  export async function isExist( req, res, next ) {
55
67
  try {
56
- const inputData=req.body;
68
+ const inputData = req.body;
57
69
  const opensearch = JSON.parse( process.env.OPENSEARCH );
58
70
  const query = {
59
71
  query: {
@@ -75,7 +87,7 @@ export async function isExist( req, res, next ) {
75
87
  };
76
88
 
77
89
  const getData = await getOpenSearchCount( opensearch.footfallDirectory, query );
78
- const isExist = getData?.body?.count == 0? true : false;
90
+ const isExist = getData?.body?.count == 0 ? true : false;
79
91
  logger.info( { isExist: isExist, count: getData?.body } );
80
92
  if ( isExist === true ) {
81
93
  next();
@@ -91,12 +103,12 @@ export async function isExist( req, res, next ) {
91
103
 
92
104
  export async function getClusters( req, res, next ) {
93
105
  try {
94
- const inputData=req.query;
106
+ const inputData = req.query;
95
107
  // const assignedStores = req.body.assignedStores;
96
108
  inputData.clientId = inputData?.clientId?.split( ',' );
97
109
  const clusters = inputData?.clusters?.split( ',' ); // convert strig to array
98
110
  // logger.info( { assignedStores, clusters } );
99
- let filter =[
111
+ let filter = [
100
112
  {
101
113
  clientId: { $in: inputData.clientId },
102
114
  },
@@ -183,7 +195,7 @@ export async function isGrantedUsers( req, res, next ) {
183
195
  const userInfo = req?.user;
184
196
  switch ( userInfo.userType ) {
185
197
  case 'client':
186
- const ticketsFeature = userInfo?.rolespermission?.find( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Reviewer' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Approver' && m.isAdd==true ) ) );
198
+ const ticketsFeature = userInfo?.rolespermission?.find( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && m.isAdd == true ) || f.modules.find( ( m ) => m.name == 'reviewer' && m.isAdd == true ) || f.modules.find( ( m ) => m.name == 'approver' && m.isAdd == true ) ) );
187
199
  logger.info( { ticketsFeature } );
188
200
  if ( ticketsFeature ) {
189
201
  return next();
@@ -208,7 +220,6 @@ export async function getConfig( req, res, next ) {
208
220
  const storeKey = inputData.storeId.split( '-' )[0];
209
221
 
210
222
  const config = await findOneClient( { clientId: storeKey }, { footfallDirectoryConfigs: 1 } );
211
- logger.info( { config, storeKey } );
212
223
  const accuracyBreach = config?.footfallDirectoryConfigs?.accuracyBreach;
213
224
  req.accuracyBreach = accuracyBreach || '';
214
225
  return next();
@@ -222,12 +233,14 @@ export async function getConfig( req, res, next ) {
222
233
  export async function ticketCreation( req, res, next ) {
223
234
  try {
224
235
  const inputData = req.body;
236
+ const sqs = JSON.parse( process.env.SQS );
237
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
225
238
  if ( inputData?.type !== 'create' ) {
226
239
  return next();
227
240
  }
228
241
  // check the createtion permission from the user permission
229
242
  const userInfo = req?.user;
230
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && ( m.isAdd==true || m.isEdit==true ) ) ) );
243
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && ( m.isAdd == true || m.isEdit == true ) ) ) );
231
244
  if ( !ticketsFeature ) {
232
245
  return res.sendError( 'Forbidden to Create Ticket', 403 );
233
246
  }
@@ -240,7 +253,6 @@ export async function ticketCreation( req, res, next ) {
240
253
  }
241
254
 
242
255
  // get the footfall count from opensearch
243
- const openSearch = JSON.parse( process.env.OPENSEARCH );
244
256
  const dateString = `${inputData.storeId}_${inputData.dateString}`;
245
257
  const getQuery = {
246
258
  query: {
@@ -265,7 +277,7 @@ export async function ticketCreation( req, res, next ) {
265
277
  }
266
278
 
267
279
  // get category details from the client level configuration
268
- const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
280
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1, clientId: 1 } );
269
281
  if ( !getConfig || getConfig == null ) {
270
282
  return res.sendError( 'The Client ID is either not configured or not found', 400 );
271
283
  }
@@ -274,7 +286,7 @@ export async function ticketCreation( req, res, next ) {
274
286
  const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
275
287
  // Initialize count object from taggingLimitation
276
288
  const tempAcc = [];
277
- const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
289
+ taggingLimitation?.reduce( ( acc, item ) => {
278
290
  if ( item?.type ) {
279
291
  // Convert type to camelCase with "Count" suffix
280
292
  // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
@@ -318,6 +330,16 @@ export async function ticketCreation( req, res, next ) {
318
330
  'dateString': inputData.dateString,
319
331
  },
320
332
  },
333
+ {
334
+ term: {
335
+ 'isParent': false,
336
+ },
337
+ },
338
+ {
339
+ term: {
340
+ isChecked: true,
341
+ },
342
+ },
321
343
  ],
322
344
  },
323
345
  },
@@ -397,25 +419,30 @@ export async function ticketCreation( req, res, next ) {
397
419
 
398
420
  const record = {
399
421
  storeId: inputData.storeId,
422
+ type: 'store',
400
423
  dateString: inputData.dateString,
401
424
  storeName: getstoreName?.storeName,
402
- ticketName: inputData.ticketName|| 'footfall-directory',
425
+ ticketName: inputData.ticketName || 'footfall-directory',
403
426
  footfallCount: footfallCount,
404
427
  clientId: getstoreName?.clientId,
405
428
  ticketId: 'TE_FDT_' + new Date().valueOf(),
406
429
  createdAt: new Date(),
407
430
  updatedAt: new Date(),
408
- status: 'raised',
431
+ status: 'Raised',
432
+ comments: inputData?.comments || '',
409
433
  revicedFootfall: revisedFootfall,
410
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
434
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
435
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
411
436
  mappingInfo: [
412
437
  {
413
438
  type: 'tagging',
414
439
  mode: inputData.mode,
415
440
  revicedFootfall: revisedFootfall,
441
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
442
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
416
443
  count: tempAcc,
417
444
  revisedDetail: formattedTaggingData,
418
- status: 'raised',
445
+ status: 'Raised',
419
446
  createdByEmail: req?.user?.email,
420
447
  createdByUserName: req?.user?.userName,
421
448
  createdByRole: req?.user?.role,
@@ -426,85 +453,92 @@ export async function ticketCreation( req, res, next ) {
426
453
 
427
454
 
428
455
  // Retrieve client footfallDirectoryConfigs revision
429
- let isAutoCloseEnable = false;
430
- let autoCloseAccuracy = '95%';
431
- try {
432
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
433
- if ( clientData?.footfallDirectoryConfigs ) {
434
- isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
435
- autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
436
- }
437
- } catch ( e ) {
438
- isAutoCloseEnable = false;
439
- autoCloseAccuracy = '95%';
440
- }
456
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
457
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
441
458
 
442
- let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || '95' ).replace( '%', '' ) );
443
- let revisedPercentage = 0;
444
- if ( typeof getCategory === 'number' && getCategory > 0 ) {
445
- revisedPercentage = ( revisedFootfall / getCategory ) * 100;
446
- }
459
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
460
+
461
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
462
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
447
463
 
448
464
  // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
449
465
  if (
450
466
  isAutoCloseEnable === true &&
451
467
  revisedPercentage >= autoCloseAccuracyValue
452
468
  ) {
453
- record.status = 'closed';
469
+ record.status = 'Closed';
454
470
  record.mappingInfo = [
455
471
  {
456
472
  type: 'tagging',
457
473
  mode: inputData.mode,
458
474
  revicedFootfall: revisedFootfall,
459
- count: getCategory,
475
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
476
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
477
+ count: tempAcc,
478
+ revisedDetail: formattedTaggingData,
479
+ status: 'Closed',
480
+ createdByEmail: req?.user?.email,
481
+ createdByUserName: req?.user?.userName,
482
+ createdByRole: req?.user?.role,
483
+ createdAt: new Date(),
484
+ },
485
+ {
486
+ type: 'finalRevision',
487
+ mode: inputData.mode,
488
+ revicedFootfall: revisedFootfall,
489
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
490
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
491
+ count: tempAcc,
460
492
  revisedDetail: formattedTaggingData,
461
- status: 'closed',
493
+ status: 'Closed',
462
494
  createdByEmail: req?.user?.email,
463
495
  createdByUserName: req?.user?.userName,
464
496
  createdByRole: req?.user?.role,
497
+ createdAt: new Date(),
465
498
  },
466
499
  ];
467
500
  } else {
468
- // If ticket is closed, do not proceed with revision mapping
501
+ // If ticket is closed, do not proceed with revision mapping
469
502
  let revisionArray = [];
470
- try {
471
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
472
- revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
473
- } catch ( e ) {
474
- revisionArray = [];
475
- }
476
503
 
504
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
477
505
  // Default fallbacks
478
506
  let revisionMapping = null;
479
507
  let approverMapping = null;
480
508
  let tangoReviewMapping = null;
481
-
482
509
  // Find out which roles have isChecked true
483
510
  if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
484
511
  for ( const r of revisionArray ) {
485
512
  if ( r.actionType === 'reviewer' && r.isChecked === true ) {
486
513
  revisionMapping = {
487
514
  type: 'review',
488
- revicedFootfall: revisedFootfall,
489
- count: getCategory,
515
+ // revicedFootfall: revisedFootfall,
516
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
517
+ count: tempAcc,
490
518
  revisedDetail: formattedTaggingData,
491
- status: 'open',
519
+ status: 'Open',
520
+ dueDate: new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
521
+
492
522
  };
493
523
  } else if ( r.actionType === 'approver' && r.isChecked === true ) {
494
524
  approverMapping = {
495
- type: 'approver',
496
- revicedFootfall: revisedFootfall,
497
- count: getCategory,
525
+ type: 'approve',
526
+ // revicedFootfall: revisedFootfall,
527
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
528
+ count: tempAcc,
498
529
  revisedDetail: formattedTaggingData,
499
- status: 'open',
530
+ status: 'Open',
531
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
500
532
  };
501
533
  } else if ( r.actionType === 'tango' && r.isChecked === true ) {
502
534
  tangoReviewMapping = {
503
- type: 'tango-review',
504
- revicedFootfall: revisedFootfall,
505
- count: getCategory,
535
+ type: 'tangoreview',
536
+ // revicedFootfall: revisedFootfall,
537
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
538
+ count: tempAcc,
506
539
  revisedDetail: formattedTaggingData,
507
- status: 'open',
540
+ status: 'Open',
541
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
508
542
  };
509
543
  }
510
544
  }
@@ -512,36 +546,179 @@ export async function ticketCreation( req, res, next ) {
512
546
 
513
547
  // Insert appropriate mappingInfo blocks
514
548
  if ( revisionMapping ) {
515
- // If reviewer and checked
549
+ // If reviewer and checked
516
550
  record.mappingInfo.push( revisionMapping );
517
551
  } else if ( approverMapping ) {
518
- // If approver and checked
552
+ // If approver and checked
519
553
  record.mappingInfo.push( approverMapping );
520
554
  } else if ( tangoReviewMapping ) {
521
- // If none above, then tangoReview
555
+ // If none above, then tangoReview
522
556
  record.mappingInfo.push( tangoReviewMapping );
523
557
  }
524
558
  }
525
559
 
560
+ const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
561
+
562
+ const hasReviewer = revision.some(
563
+ ( data ) => data.actionType === 'reviewer' && data.isChecked === true,
564
+ );
565
+ const hasApprover = revision.some(
566
+ ( data ) => data.actionType === 'approver' && data.isChecked === true,
567
+ );
568
+
569
+ if ( hasReviewer || hasApprover ) {
570
+ const userQuery = [
571
+ {
572
+ $match: {
573
+ clientId: getstoreName.clientId,
574
+ role: 'admin',
575
+ isActive: true,
576
+ },
577
+ },
578
+ ];
579
+
580
+ const finduserList = await aggregateUser( userQuery );
581
+
582
+
583
+ const createdOn = dayjs().format( 'DD MMM YYYY' );
584
+ const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
585
+ const description = `Created on ${createdOn}`;
586
+
587
+ const Data = {
588
+ title,
589
+ body: description,
590
+ type: 'create',
591
+ date: record.dateString,
592
+ storeId: record.storeId,
593
+ clientId: record.clientId,
594
+ ticketId: record.ticketId,
595
+ };
596
+
597
+ await Promise.all(
598
+ ( finduserList || [] ).map( async ( userData ) => {
599
+ const ticketsFeature = userData?.rolespermission?.some(
600
+ ( f ) =>
601
+ f.featureName === 'FootfallDirectory' &&
602
+ f.modules?.some(
603
+ ( m ) =>
604
+ m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
605
+ ),
606
+ );
607
+
608
+
609
+ if ( !ticketsFeature ) return;
610
+
611
+ const notifyUser = await getAssinedStore( userData, req.body.storeId );
612
+ if ( !notifyUser || !userData?.fcmToken ) return;
613
+
614
+ await sendPushNotification( title, description, userData.fcmToken, Data );
615
+ } ),
616
+ );
617
+ }
618
+
526
619
 
527
620
  const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
528
621
  const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
529
622
  if ( insertResult && insertResult.statusCode === 201 ) {
530
623
  // After successful ticket creation, update status to "submitted" in revop index for the relevant records
531
- try {
532
- const bulkUpdateBody = taggingImages.map( ( img ) => [
533
- { update: { _index: openSearch.revop, _id: img._id } },
534
- { doc: { status: 'submitted' } },
535
- ] ).flat();
536
-
537
- if ( bulkUpdateBody.length > 0 ) {
538
- await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
539
- }
540
- } catch ( updateErr ) {
541
- logger.error( { error: updateErr, message: 'Failed to update status to submitted in revop index' } );
542
- // Do not block the success response for this failure
624
+
625
+
626
+ const bulkUpdateBody = taggingImages.map( ( img ) => [
627
+ { update: { _index: openSearch.revop, _id: img._id } },
628
+ { doc: { status: 'submitted' } },
629
+ ] ).flat();
630
+
631
+ if ( bulkUpdateBody.length > 0 ) {
632
+ await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
543
633
  }
544
634
 
635
+ if ( record.status = 'Closed' ) {
636
+ const query = {
637
+ storeId: inputData?.storeId,
638
+ isVideoStream: true,
639
+ };
640
+ const getStoreType = await countDocumnetsCamera( query );
641
+ const revopInfoQuery = {
642
+ size: 10000,
643
+ query: {
644
+ bool: {
645
+ must: [
646
+ {
647
+ term: {
648
+ 'storeId.keyword': inputData.storeId,
649
+ },
650
+ },
651
+ {
652
+ term: {
653
+ 'dateString': inputData.dateString,
654
+ },
655
+ },
656
+ {
657
+ term: {
658
+ 'isParent': false,
659
+ },
660
+ },
661
+ {
662
+ term: {
663
+ isChecked: true,
664
+ },
665
+ },
666
+ ],
667
+ },
668
+ },
669
+ _source: [ 'tempId' ],
670
+
671
+ };
672
+
673
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
674
+
675
+ // Get all tempIds from revopInfo response
676
+ const tempIds =
677
+ revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
678
+ // Prepare management eyeZone query based on storeId and dateString
679
+ const managerEyeZoneQuery = {
680
+ size: 1,
681
+ query: {
682
+ bool: {
683
+ must: [
684
+ {
685
+ term: {
686
+ 'storeId.keyword': inputData.storeId,
687
+ },
688
+ },
689
+ {
690
+ term: {
691
+ 'storeDate': inputData.dateString,
692
+ },
693
+ },
694
+ ],
695
+ },
696
+ },
697
+ _source: [ 'originalToTrackerCustomerMapping' ],
698
+ };
699
+
700
+ // Query the managerEyeZone index for the matching document
701
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
702
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
703
+ // Extract originalToTrackerCustomerMapping if it exists
704
+ const mapping =
705
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
706
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
707
+ {};
708
+
709
+ // If you want to compare or find matching tempIds in the mapping
710
+ // The mapping is { "1": tempId1, ... }, so get values as array of tempIds
711
+ // const managerMappedTempIds = Object.values( mapping );
712
+
713
+ // Find tempIds that exist in both revopInfo results and manager mapping
714
+ const temp = [];
715
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
716
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
717
+ if ( isSendMessge == true ) {
718
+ logger.info( '....1' );
719
+ // return true; // res.sendSuccess( 'Ticket has been updated successfully' );
720
+ } // Example: log or use these tempIds for further logic
721
+ }
545
722
  // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
546
723
 
547
724
  if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
@@ -551,7 +728,7 @@ export async function ticketCreation( req, res, next ) {
551
728
  // req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
552
729
  const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
553
730
  const storeId = inputData.storeId;
554
- const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
731
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
555
732
 
556
733
 
557
734
  const formatDate = ( d ) =>
@@ -665,14 +842,1233 @@ export async function ticketCreation( req, res, next ) {
665
842
  }
666
843
  }
667
844
  }
845
+ const sqsName = sqs.vmsPickleExtention;
846
+ const sqsProduceQueue = {
847
+ QueueUrl: `${sqs.url}${sqsName}`,
848
+ MessageBody: JSON.stringify( {
849
+ store_id: inputData?.storeId,
850
+ store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
851
+ primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
852
+ time: new Date(),
853
+ } ),
854
+ MessageGroupId: 'revops-pickle',
855
+ MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
856
+ };
857
+ const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
858
+
859
+ if ( sqsQueue.statusCode ) {
860
+ logger.error( {
861
+ error: `${sqsQueue}`,
862
+ type: 'SQS_NOT_SEND_ERROR',
863
+ } );
864
+ }
865
+
668
866
 
669
867
  return res.sendSuccess( 'Ticket raised successfully' );
670
868
  }
671
869
  } catch ( error ) {
672
870
  const err = error.message || 'Internal Server Error';
673
- logger.error( { error: err, funtion: 'ticketCreation' } );
871
+ logger.error( { error: error, funtion: 'ticketCreation' } );
674
872
  return res.sendError( err, 500 );
675
873
  }
676
874
  }
677
875
 
876
+ export async function ticketReview( req, res, next ) {
877
+ try {
878
+ const inputData = req.body;
879
+ if ( inputData?.type !== 'review' ) {
880
+ return next();
881
+ }
882
+ // check the createtion permission from the user permission
883
+ const userInfo = req?.user;
884
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
885
+ if ( !ticketsFeature ) {
886
+ return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
887
+ }
888
+
889
+ // get store info by the storeId into mongo db
890
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
891
+
892
+ if ( !getstoreName || getstoreName == null ) {
893
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
894
+ }
895
+
896
+ // get the footfall count from opensearch
897
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
898
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
899
+ const getQuery = {
900
+ query: {
901
+ terms: {
902
+ _id: [ dateString ],
903
+ },
904
+ },
905
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
906
+ sort: [
907
+ {
908
+ date_iso: {
909
+ order: 'desc',
910
+ },
911
+ },
912
+ ],
913
+ };
914
+
915
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
916
+ const hits = getFootfallCount?.body?.hits?.hits || [];
917
+ logger.info( { hits } );
918
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
919
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
920
+ }
921
+
922
+ // get category details from the client level configuration
923
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
924
+ if ( !getConfig || getConfig == null ) {
925
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
926
+ }
927
+
928
+ // Get taggingLimitation from config (check both possible paths)
929
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
930
+ // Initialize count object from taggingLimitation
931
+ const tempAcc = [];
932
+ taggingLimitation?.reduce( ( acc, item ) => {
933
+ if ( item?.type ) {
934
+ // Convert type to camelCase with "Count" suffix
935
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
936
+ const typeLower = item.type.toLowerCase();
937
+ let key;
938
+ if ( typeLower === 'housekeeping' ) {
939
+ key = 'houseKeepingCount';
940
+ } else {
941
+ // Convert first letter to lowercase and append "Count"
942
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
943
+ }
944
+
945
+
946
+ // To change from an object to the desired array structure, assemble an array of objects:
947
+ tempAcc.push( {
948
+ name: item.name,
949
+ value: 0,
950
+ key: key,
951
+ type: item.type,
952
+ } );
953
+
954
+
955
+ return acc;
956
+ }
957
+ }, {} ) || {};
958
+
959
+ // Query OpenSearch revop index to get actual counts for each type
960
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
961
+ const revopQuery = {
962
+ size: 0,
963
+ query: {
964
+ bool: {
965
+ must: [
966
+ {
967
+ term: {
968
+ 'storeId.keyword': inputData.storeId,
969
+ },
970
+ },
971
+ {
972
+ term: {
973
+ 'dateString': inputData.dateString,
974
+ },
975
+ },
976
+ {
977
+ term: {
978
+ 'isParent': false,
979
+ },
980
+ },
981
+ {
982
+ term: {
983
+ isChecked: true,
984
+ },
985
+ },
986
+ ],
987
+ },
988
+ },
989
+ aggs: {
990
+ type_counts: {
991
+ terms: {
992
+ field: 'revopsType.keyword',
993
+ size: 100,
994
+ },
995
+ },
996
+ },
997
+ };
998
+
999
+
1000
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1001
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1002
+
1003
+ // Map OpenSearch revopsType values to count object keys
1004
+ buckets.forEach( ( bucket ) => {
1005
+ const revopsType = bucket.key;
1006
+ const count = bucket.doc_count || 0;
1007
+
1008
+
1009
+ if ( Array.isArray( tempAcc ) ) {
1010
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1011
+ const accMatch = tempAcc.find(
1012
+ ( acc ) =>
1013
+ acc.type &&
1014
+ acc.type === revopsType,
1015
+ );
1016
+
1017
+ if ( accMatch && accMatch.key ) {
1018
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1019
+ }
1020
+ }
1021
+ } );
1022
+ }
1023
+
1024
+
1025
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1026
+
1027
+ const totalCount = Array.isArray( tempAcc ) ?
1028
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1029
+ 0;
1030
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1031
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1032
+ if ( footfallCount - revisedFootfall == 0 ) {
1033
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1034
+ }
1035
+ const taggingData = {
1036
+ size: 10000,
1037
+ query: {
1038
+ bool: {
1039
+ must: [
1040
+ {
1041
+ term: {
1042
+ 'storeId.keyword': inputData.storeId,
1043
+ },
1044
+ },
1045
+ {
1046
+ term: {
1047
+ 'dateString': inputData.dateString,
1048
+ },
1049
+ },
1050
+ ],
1051
+ },
1052
+ },
1053
+ };
1054
+
1055
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1056
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1057
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1058
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1059
+ }
1060
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1061
+
1062
+ const getTicket = {
1063
+ size: 10000,
1064
+ query: {
1065
+ bool: {
1066
+ must: [
1067
+ {
1068
+ term: {
1069
+ 'storeId.keyword': inputData.storeId,
1070
+ },
1071
+ },
1072
+ {
1073
+ term: {
1074
+ 'dateString': inputData.dateString,
1075
+ },
1076
+ },
1077
+ ],
1078
+ },
1079
+ },
1080
+ };
1081
+
1082
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1083
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1084
+ if ( !ticketData || ticketData?.length == 0 ) {
1085
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1086
+ }
1087
+ const record = {
1088
+
1089
+ status: 'Reviewer-Closed',
1090
+ revicedFootfall: revisedFootfall,
1091
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1092
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1093
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1094
+ // createdByEmail: req?.user?.email,
1095
+ // createdByUserName: req?.user?.userName,
1096
+ // createdByRole: req?.user?.role,
1097
+
1098
+ };
1099
+
1100
+ if ( Array.isArray( record.mappingInfo ) ) {
1101
+ const temp = record.mappingInfo
1102
+ .filter( ( item ) => item.type === 'review' )
1103
+ .map( ( item ) => ( {
1104
+ ...item,
1105
+ mode: inputData.mode,
1106
+ revicedFootfall: revisedFootfall,
1107
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1108
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1109
+ count: tempAcc,
1110
+ revisedDetail: formattedTaggingData,
1111
+ status: 'Closed',
1112
+ createdByEmail: req?.user?.email,
1113
+ createdByUserName: req?.user?.userName,
1114
+ createdByRole: req?.user?.role,
1115
+ createdAt: new Date(),
1116
+ } ) );
1117
+ record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
1118
+ // If no review mapping existed, push a new one
1119
+ if ( record.mappingInfo.length === 0 ) {
1120
+ record.mappingInfo.push( {
1121
+ type: 'review',
1122
+ mode: inputData.mode,
1123
+ revicedFootfall: revisedFootfall,
1124
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1125
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1126
+ count: tempAcc,
1127
+ revisedDetail: formattedTaggingData,
1128
+ status: 'Closed',
1129
+ createdByEmail: req?.user?.email,
1130
+ createdByUserName: req?.user?.userName,
1131
+ createdByRole: req?.user?.role,
1132
+ createdAt: new Date(),
1133
+ } );
1134
+ }
1135
+ }
1136
+
1137
+
1138
+ // Retrieve client footfallDirectoryConfigs revision
1139
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
1140
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1141
+
1142
+
1143
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1144
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1145
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1146
+
1147
+
1148
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1149
+ if (
1150
+ isAutoCloseEnable === true &&
1151
+ revisedPercentage >= autoCloseAccuracyValue
1152
+ ) {
1153
+ record.status = 'Reviewer-Closed';
1154
+ // Only keep or modify mappingInfo items with type "review"
1155
+ if ( Array.isArray( record.mappingInfo ) ) {
1156
+ const temp = record.mappingInfo
1157
+ .filter( ( item ) => item.type === 'review' )
1158
+ .map( ( item ) => ( {
1159
+ ...item,
1160
+ mode: inputData.mode,
1161
+ revicedFootfall: revisedFootfall,
1162
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1163
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1164
+ count: tempAcc,
1165
+ revisedDetail: formattedTaggingData,
1166
+ status: 'Closed',
1167
+ createdByEmail: req?.user?.email,
1168
+ createdByUserName: req?.user?.userName,
1169
+ createdByRole: req?.user?.role,
1170
+ } ) );
1171
+
1172
+ const temp2 = record.mappingInfo
1173
+ .filter( ( item ) => item.type === 'tagging' )
1174
+ .map( ( item ) => ( {
1175
+ ...item,
1176
+ mode: inputData.mode,
1177
+ // revicedFootfall: revisedFootfall,
1178
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1179
+ // count: tempAcc,
1180
+ // revisedDetail: formattedTaggingData,
1181
+ status: 'Closed',
1182
+ // createdByEmail: req?.user?.email,
1183
+ // createdByUserName: req?.user?.userName,
1184
+ // createdByRole: req?.user?.role,
1185
+ } ) );
1186
+ record.mappingInfo = [ ...temp2, ...temp ];
1187
+ // If no review mapping existed, push a new one
1188
+ if ( record.mappingInfo.length === 0 ) {
1189
+ record.mappingInfo.push( {
1190
+ type: 'review',
1191
+ mode: inputData.mode,
1192
+ revicedFootfall: revisedFootfall,
1193
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1194
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1195
+ count: tempAcc,
1196
+ revisedDetail: formattedTaggingData,
1197
+ status: 'Closed',
1198
+ createdByEmail: req?.user?.email,
1199
+ createdByUserName: req?.user?.userName,
1200
+ createdByRole: req?.user?.role,
1201
+ } );
1202
+ }
1203
+ }
1204
+ record.mappingInfo.push(
1205
+ {
1206
+ type: 'finalRevision',
1207
+ mode: inputData.mode,
1208
+ revicedFootfall: revisedFootfall,
1209
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1210
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1211
+ count: tempAcc,
1212
+ revisedDetail: formattedTaggingData,
1213
+ status: 'Closed',
1214
+ createdByEmail: req?.user?.email,
1215
+ createdByUserName: req?.user?.userName,
1216
+ createdByRole: req?.user?.role,
1217
+ createdAt: new Date(),
1218
+ },
1219
+ );
1220
+ } else {
1221
+ // If ticket is closed, do not proceed with revision mapping
1222
+ let revisionArray = [];
1223
+
1224
+
1225
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
1226
+
1227
+
1228
+ // Default fallbacks
1229
+ let revisionMapping = null;
1230
+ let approverMapping = null;
1231
+ let tangoReviewMapping = null;
1232
+
1233
+ // Find out which roles have isChecked true
1234
+ if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
1235
+ for ( const r of revisionArray ) {
1236
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
1237
+ approverMapping = {
1238
+ type: 'approve',
1239
+ // revicedFootfall: revisedFootfall,
1240
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1241
+ count: tempAcc,
1242
+ revisedDetail: formattedTaggingData,
1243
+ status: 'Open',
1244
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
1245
+ };
1246
+ } else if ( r.actionType === 'tango' && r.isChecked === true ) {
1247
+ tangoReviewMapping = {
1248
+ type: 'tangoreview',
1249
+ // revicedFootfall: revisedFootfall,
1250
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1251
+ count: tempAcc,
1252
+ revisedDetail: formattedTaggingData,
1253
+ status: 'Open',
1254
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
1255
+ };
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ // Insert appropriate mappingInfo blocks
1261
+ if ( revisionMapping ) {
1262
+ // If reviewer and checked
1263
+ record.mappingInfo.push( revisionMapping );
1264
+ } else if ( approverMapping ) {
1265
+ // If approver and checked
1266
+ record.mappingInfo.push( approverMapping );
1267
+ } else if ( tangoReviewMapping ) {
1268
+ // If none above, then tangoReview
1269
+ record.mappingInfo.push( tangoReviewMapping );
1270
+ }
1271
+ }
1272
+
1273
+ let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
1274
+
1275
+
1276
+ if ( checkreview.length > 0 ) {
1277
+ let userQuery = [
1278
+ {
1279
+ $match: {
1280
+ clientId: getstoreName.clientId,
1281
+ role: 'admin',
1282
+ isActive: true,
1283
+ },
1284
+ },
1285
+ ];
1286
+ let finduserList = await aggregateUser( userQuery );
1287
+
1288
+
1289
+ // return;
1290
+ for ( let userData of finduserList ) {
1291
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1292
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1293
+ let description = `Created on ${createdOn}`;
1294
+
1295
+ let Data = {
1296
+ 'title': title,
1297
+ 'body': description,
1298
+ 'type': 'review',
1299
+ 'date': record.dateString,
1300
+ 'storeId': record.storeId,
1301
+ 'clientId': record.clientId,
1302
+ 'ticketId': record.ticketId,
1303
+ };
1304
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1305
+
1306
+ if ( ticketsFeature ) {
1307
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1308
+ if ( userData && userData.fcmToken && notifyuser ) {
1309
+ const fcmToken = userData.fcmToken;
1310
+ await sendPushNotification( title, description, fcmToken, Data );
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1317
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1318
+
1319
+ if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
1320
+ if ( record.status = 'Closed' ) {
1321
+ const query = {
1322
+ storeId: inputData?.storeId,
1323
+ isVideoStream: true,
1324
+ };
1325
+ const getStoreType = await countDocumnetsCamera( query );
1326
+ const revopInfoQuery = {
1327
+ size: 10000,
1328
+ query: {
1329
+ bool: {
1330
+ must: [
1331
+ {
1332
+ term: {
1333
+ 'storeId.keyword': inputData.storeId,
1334
+ },
1335
+ },
1336
+ {
1337
+ term: {
1338
+ 'dateString': inputData.dateString,
1339
+ },
1340
+ },
1341
+ {
1342
+ term: {
1343
+ 'isParent': false,
1344
+ },
1345
+ },
1346
+ {
1347
+ term: {
1348
+ isChecked: true,
1349
+ },
1350
+ },
1351
+ ],
1352
+ },
1353
+ },
1354
+ _source: [ 'tempId' ],
1355
+
1356
+ };
1357
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
1358
+ // Get all tempIds from revopInfo response
1359
+ const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
1360
+ // Prepare management eyeZone query based on storeId and dateString
1361
+ const managerEyeZoneQuery = {
1362
+ size: 1,
1363
+ query: {
1364
+ bool: {
1365
+ must: [
1366
+ {
1367
+ term: {
1368
+ 'storeId.keyword': inputData.storeId,
1369
+ },
1370
+ },
1371
+ {
1372
+ term: {
1373
+ 'storeDate': inputData.dateString,
1374
+ },
1375
+ },
1376
+ ],
1377
+ },
1378
+ },
1379
+ _source: [ 'originalToTrackerCustomerMapping' ],
1380
+ };
1381
+
1382
+ // Query the managerEyeZone index for the matching document
1383
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
1384
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
1385
+ // Extract originalToTrackerCustomerMapping if it exists
1386
+ const mapping =
1387
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
1388
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
1389
+ {};
1390
+
1391
+ // Find tempIds that exist in both revopInfo results and manager mapping
1392
+ const temp = [];
1393
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
1394
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
1395
+ if ( isSendMessge == true ) {
1396
+ logger.info( '....1' );
1397
+ }
1398
+ }
1399
+ return res.sendSuccess( 'Ticket closed successfully' );
1400
+ } else {
1401
+ return res.sendError( 'Internal Server Error', 500 );
1402
+ }
1403
+ } catch ( error ) {
1404
+ const err = error.message || 'Internal Server Error';
1405
+ logger.error( { error: err, funtion: 'ticketreview' } );
1406
+ return res.sendError( err, 500 );
1407
+ }
1408
+ }
1409
+
1410
+ export async function ticketApprove( req, res, next ) {
1411
+ try {
1412
+ const inputData = req.body;
1413
+ if ( inputData?.type !== 'approve' ) {
1414
+ return next();
1415
+ }
1416
+ // check the createtion permission from the user permission
1417
+ const userInfo = req?.user;
1418
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1419
+ if ( !ticketsFeature ) {
1420
+ return res.sendError( 'Forbidden to Approve this Ticket', 403 );
1421
+ }
1422
+
1423
+ // get store info by the storeId into mongo db
1424
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
1425
+
1426
+ if ( !getstoreName || getstoreName == null ) {
1427
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
1428
+ }
1429
+
1430
+ // get the footfall count from opensearch
1431
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1432
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
1433
+ const getQuery = {
1434
+ query: {
1435
+ terms: {
1436
+ _id: [ dateString ],
1437
+ },
1438
+ },
1439
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
1440
+ sort: [
1441
+ {
1442
+ date_iso: {
1443
+ order: 'desc',
1444
+ },
1445
+ },
1446
+ ],
1447
+ };
1448
+
1449
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
1450
+ const hits = getFootfallCount?.body?.hits?.hits || [];
1451
+ logger.info( { hits } );
1452
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
1453
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
1454
+ }
1455
+
1456
+ // get category details from the client level configuration
1457
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
1458
+ if ( !getConfig || getConfig == null ) {
1459
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
1460
+ }
1461
+
1462
+ // Get taggingLimitation from config (check both possible paths)
1463
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
1464
+ // Initialize count object from taggingLimitation
1465
+ const tempAcc = [];
1466
+ taggingLimitation?.reduce( ( acc, item ) => {
1467
+ if ( item?.type ) {
1468
+ // Convert type to camelCase with "Count" suffix
1469
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
1470
+ const typeLower = item.type.toLowerCase();
1471
+ let key;
1472
+ if ( typeLower === 'housekeeping' ) {
1473
+ key = 'houseKeepingCount';
1474
+ } else {
1475
+ // Convert first letter to lowercase and append "Count"
1476
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
1477
+ }
1478
+
1479
+
1480
+ // To change from an object to the desired array structure, assemble an array of objects:
1481
+ tempAcc.push( {
1482
+ name: item.name,
1483
+ value: 0,
1484
+ key: key,
1485
+ type: item.type,
1486
+ } );
1487
+
1488
+
1489
+ return acc;
1490
+ }
1491
+ }, {} ) || {};
1492
+
1493
+ // Query OpenSearch revop index to get actual counts for each type
1494
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
1495
+ const revopQuery = {
1496
+ size: 0,
1497
+ query: {
1498
+ bool: {
1499
+ must: [
1500
+ {
1501
+ term: {
1502
+ 'storeId.keyword': inputData.storeId,
1503
+ },
1504
+ },
1505
+ {
1506
+ term: {
1507
+ 'dateString': inputData.dateString,
1508
+ },
1509
+ },
1510
+ ],
1511
+ },
1512
+ },
1513
+ aggs: {
1514
+ type_counts: {
1515
+ terms: {
1516
+ field: 'revopsType.keyword',
1517
+ size: 100,
1518
+ },
1519
+ },
1520
+ },
1521
+ };
1522
+
1523
+
1524
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1525
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1526
+
1527
+ // Map OpenSearch revopsType values to count object keys
1528
+ buckets.forEach( ( bucket ) => {
1529
+ const revopsType = bucket.key;
1530
+ const count = bucket.doc_count || 0;
1531
+
1532
+
1533
+ if ( Array.isArray( tempAcc ) ) {
1534
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1535
+ const accMatch = tempAcc.find(
1536
+ ( acc ) =>
1537
+ acc.type &&
1538
+ acc.type === revopsType,
1539
+ );
1540
+
1541
+ if ( accMatch && accMatch.key ) {
1542
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1543
+ }
1544
+ }
1545
+ } );
1546
+ }
1547
+
1548
+
1549
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1550
+
1551
+ const totalCount = Array.isArray( tempAcc ) ?
1552
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1553
+ 0;
1554
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1555
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1556
+ logger.info( { footfallCount, revisedFootfall } );
1557
+ if ( footfallCount - revisedFootfall == 0 ) {
1558
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1559
+ }
1560
+
1561
+ const taggingData = {
1562
+ size: 10000,
1563
+ query: {
1564
+ bool: {
1565
+ must: [
1566
+ {
1567
+ term: {
1568
+ 'storeId.keyword': inputData.storeId,
1569
+ },
1570
+ },
1571
+ {
1572
+ term: {
1573
+ 'dateString': inputData.dateString,
1574
+ },
1575
+ },
1576
+ {
1577
+ term: {
1578
+ 'isParent': false,
1579
+ },
1580
+ },
1581
+ {
1582
+ term: {
1583
+ isChecked: true,
1584
+ },
1585
+ },
1586
+ ],
1587
+ },
1588
+ },
1589
+ };
1590
+
1591
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1592
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1593
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1594
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1595
+ }
1596
+
1597
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1598
+
1599
+ const getTicket = {
1600
+ size: 10000,
1601
+ query: {
1602
+ bool: {
1603
+ must: [
1604
+ {
1605
+ term: {
1606
+ 'storeId.keyword': inputData.storeId,
1607
+ },
1608
+ },
1609
+ {
1610
+ term: {
1611
+ 'dateString': inputData.dateString,
1612
+ },
1613
+ },
1614
+ ],
1615
+ },
1616
+ },
1617
+ };
1618
+
1619
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1620
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1621
+ if ( !ticketData || ticketData?.length == 0 ) {
1622
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1623
+ }
1624
+ logger.info( { ticketData, getFootfallticketData } );
1625
+ const record = {
1626
+
1627
+ status: 'Approver-Closed',
1628
+ revicedFootfall: revisedFootfall,
1629
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1630
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1631
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1632
+ // createdByEmail: req?.user?.email,
1633
+ // createdByUserName: req?.user?.userName,
1634
+ // createdByRole: req?.user?.role,
1635
+
1636
+ };
1637
+
1638
+
1639
+ // Retrieve client footfallDirectoryConfigs revision
1640
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
1641
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1642
+
1643
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1644
+ logger.info( { getNumber } );
1645
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1646
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1647
+ const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
1648
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
1649
+ logger.info( { tangoReview, revised } );
1650
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1651
+ if (
1652
+ isAutoCloseEnable === true &&
1653
+ revisedPercentage >= autoCloseAccuracyValue
1654
+ ) {
1655
+ record.status = 'Approver-Closed';
1656
+ // Only keep or modify mappingInfo items with type "review"
1657
+ if ( Array.isArray( record.mappingInfo ) ) {
1658
+ const temp = record.mappingInfo
1659
+ .filter( ( item ) => item.type === 'approve' )
1660
+ .map( ( item ) => ( {
1661
+ ...item,
1662
+
1663
+ mode: inputData.mode,
1664
+ revicedFootfall: revisedFootfall,
1665
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1666
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1667
+ count: tempAcc,
1668
+ revisedDetail: formattedTaggingData,
1669
+ status: 'Closed',
1670
+ createdByEmail: req?.user?.email,
1671
+ createdByUserName: req?.user?.userName,
1672
+ createdByRole: req?.user?.role,
1673
+ } ) );
1674
+
1675
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1676
+ ...temp ];
1677
+ // If updating the mapping config to mark [i].status as 'Closed'
1678
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
1679
+ if ( Array.isArray( record.mappingInfo ) ) {
1680
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1681
+ return {
1682
+ ...item,
1683
+ status: 'Closed',
1684
+ };
1685
+ } );
1686
+ }
1687
+ // If no review mapping existed, push a new one
1688
+ if ( record.mappingInfo.length === 0 ) {
1689
+ record.mappingInfo.push( {
1690
+ type: 'approve',
1691
+ mode: inputData.mode,
1692
+ revicedFootfall: revisedFootfall,
1693
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1694
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1695
+ count: tempAcc,
1696
+ revisedDetail: formattedTaggingData,
1697
+ status: 'Closed',
1698
+ createdByEmail: req?.user?.email,
1699
+ createdByUserName: req?.user?.userName,
1700
+ createdByRole: req?.user?.role,
1701
+ } );
1702
+ }
1703
+ }
1704
+ record.mappingInfo.push(
1705
+ {
1706
+ type: 'finalRevision',
1707
+ mode: inputData.mode,
1708
+ revicedFootfall: revisedFootfall,
1709
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1710
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1711
+ count: tempAcc,
1712
+ revisedDetail: formattedTaggingData,
1713
+ status: 'Closed',
1714
+ createdByEmail: req?.user?.email,
1715
+ createdByUserName: req?.user?.userName,
1716
+ createdByRole: req?.user?.role,
1717
+ createdAt: new Date(),
1718
+ },
1719
+ );
1720
+ } else if ( revised < tangoReview ) {
1721
+ // If ticket is closed, do not proceed with revision mapping
1722
+
1723
+ // Default fallbacks
1724
+
1725
+ let approverMapping = null;
1726
+ let tangoReviewMapping = null;
1727
+
1728
+ record.status = 'Approver-Closed';
1729
+ // Only keep or modify mappingInfo items with type "review"
1730
+ if ( Array.isArray( record.mappingInfo ) ) {
1731
+ const temp = record.mappingInfo
1732
+ .filter( ( item ) => item.type === 'approve' )
1733
+ .map( ( item ) => ( {
1734
+ ...item,
1735
+ mode: inputData.mode,
1736
+ revicedFootfall: revisedFootfall,
1737
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1738
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1739
+ count: tempAcc,
1740
+ revisedDetail: formattedTaggingData,
1741
+ status: 'Under Tango Review',
1742
+ createdByEmail: req?.user?.email,
1743
+ createdByUserName: req?.user?.userName,
1744
+ createdByRole: req?.user?.role,
1745
+ } ) );
1746
+
1747
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1748
+ ...temp ];
1749
+
1750
+ // If no review mapping existed, push a new one
1751
+ if ( record.mappingInfo.length === 0 ) {
1752
+ record.mappingInfo.push( {
1753
+ type: 'approve',
1754
+ mode: inputData.mode,
1755
+ revicedFootfall: revisedFootfall,
1756
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1757
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1758
+ count: tempAcc,
1759
+ revisedDetail: formattedTaggingData,
1760
+ status: 'Closed',
1761
+ createdByEmail: req?.user?.email,
1762
+ createdByUserName: req?.user?.userName,
1763
+ createdByRole: req?.user?.role,
1764
+ } );
1765
+ }
1766
+ }
1767
+
1768
+ // Find out which roles have isChecked true
1769
+
1770
+ // for ( const r of revisionArray ) {
1771
+ // if ( r.actionType === 'tango' && r.isChecked === true ) {
1772
+ tangoReviewMapping = {
1773
+ type: 'tangoreview',
1774
+ // revicedFootfall: revisedFootfall,
1775
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1776
+ count: tempAcc,
1777
+ revisedDetail: formattedTaggingData,
1778
+ status: 'Open',
1779
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
1780
+ };
1781
+ // }
1782
+ // }
1783
+
1784
+
1785
+ if ( approverMapping ) {
1786
+ // If approver and checked
1787
+ record.mappingInfo.push( approverMapping );
1788
+ } else if ( tangoReviewMapping ) {
1789
+ // If none above, then tangoReview
1790
+ record.mappingInfo.push( tangoReviewMapping );
1791
+ }
1792
+ } else {
1793
+ if ( Array.isArray( record.mappingInfo ) ) {
1794
+ const temp = record.mappingInfo
1795
+ .filter( ( item ) => item.type === 'approve' )
1796
+ .map( ( item ) => ( {
1797
+ ...item,
1798
+ mode: inputData.mode,
1799
+ revicedFootfall: revisedFootfall,
1800
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1801
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1802
+ count: tempAcc,
1803
+ revisedDetail: formattedTaggingData,
1804
+ status: 'Closed',
1805
+ createdByEmail: req?.user?.email,
1806
+ createdByUserName: req?.user?.userName,
1807
+ createdByRole: req?.user?.role,
1808
+ } ) );
1809
+
1810
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1811
+ ...temp ];
1812
+ if ( Array.isArray( record.mappingInfo ) ) {
1813
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1814
+ return {
1815
+ ...item,
1816
+ status: 'Closed',
1817
+ };
1818
+ } );
1819
+ }
1820
+ // If no review mapping existed, push a new one
1821
+ if ( record.mappingInfo.length === 0 ) {
1822
+ record.mappingInfo.push( {
1823
+ type: 'approve',
1824
+ mode: inputData.mode,
1825
+ revicedFootfall: revisedFootfall,
1826
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1827
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1828
+ count: tempAcc,
1829
+ revisedDetail: formattedTaggingData,
1830
+ status: 'Closed',
1831
+ createdByEmail: req?.user?.email,
1832
+ createdByUserName: req?.user?.userName,
1833
+ createdByRole: req?.user?.role,
1834
+ } );
1835
+ }
1836
+ }
1837
+ record.mappingInfo.push(
1838
+ {
1839
+ type: 'finalRevision',
1840
+ mode: inputData.mode,
1841
+ revicedFootfall: revisedFootfall,
1842
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1843
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1844
+ count: tempAcc,
1845
+ revisedDetail: formattedTaggingData,
1846
+ status: 'Closed',
1847
+ createdByEmail: req?.user?.email,
1848
+ createdByUserName: req?.user?.userName,
1849
+ createdByRole: req?.user?.role,
1850
+ createdAt: new Date(),
1851
+ },
1852
+ );
1853
+ }
1854
+ console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
1855
+ let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
1856
+
1857
+
1858
+ if ( checkapprove.length > 0 ) {
1859
+ let userQuery = [
1860
+ {
1861
+ $match: {
1862
+ clientId: getstoreName.clientId,
1863
+ role: 'admin',
1864
+ isActive: true,
1865
+ },
1866
+ },
1867
+ ];
1868
+ let finduserList = await aggregateUser( userQuery );
1869
+
1870
+
1871
+ for ( let userData of finduserList ) {
1872
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1873
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1874
+ let description = `Created on ${createdOn}`;
1875
+
1876
+ let Data = {
1877
+ 'title': title,
1878
+ 'body': description,
1879
+ 'type': 'approve',
1880
+ 'date': record.dateString,
1881
+ 'storeId': record.storeId,
1882
+ 'clientId': record.clientId,
1883
+ 'ticketId': record.ticketId,
1884
+ };
1885
+
1886
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1887
+
1888
+ if ( ticketsFeature ) {
1889
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1890
+ if ( userData && userData.fcmToken && notifyuser ) {
1891
+ const fcmToken = userData.fcmToken;
1892
+ await sendPushNotification( title, description, fcmToken, Data );
1893
+ }
1894
+ }
1895
+ }
1896
+ }
1897
+
1898
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1899
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1900
+
1901
+
1902
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
1903
+ if ( record.status = 'Closed' ) {
1904
+ const query = {
1905
+ storeId: inputData?.storeId,
1906
+ isVideoStream: true,
1907
+ };
1908
+ const getStoreType = await countDocumnetsCamera( query );
1909
+ const revopInfoQuery = {
1910
+ size: 10000,
1911
+ query: {
1912
+ bool: {
1913
+ must: [
1914
+ {
1915
+ term: {
1916
+ 'storeId.keyword': inputData.storeId,
1917
+ },
1918
+ },
1919
+ {
1920
+ term: {
1921
+ 'dateString': inputData.dateString,
1922
+ },
1923
+ },
1924
+ {
1925
+ term: {
1926
+ 'isParent': false,
1927
+ },
1928
+ },
1929
+ {
1930
+ term: {
1931
+ isChecked: true,
1932
+ },
1933
+ },
1934
+ ],
1935
+ },
1936
+ },
1937
+ _source: [ 'tempId' ],
1938
+
1939
+ };
1940
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
1941
+ // Get all tempIds from revopInfo response
1942
+ const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
1943
+ // Prepare management eyeZone query based on storeId and dateString
1944
+ const managerEyeZoneQuery = {
1945
+ size: 1,
1946
+ query: {
1947
+ bool: {
1948
+ must: [
1949
+ {
1950
+ term: {
1951
+ 'storeId.keyword': inputData.storeId,
1952
+ },
1953
+ },
1954
+ {
1955
+ term: {
1956
+ 'storeDate': inputData.dateString,
1957
+ },
1958
+ },
1959
+ ],
1960
+ },
1961
+ },
1962
+ _source: [ 'originalToTrackerCustomerMapping' ],
1963
+ };
1964
+
1965
+ // Query the managerEyeZone index for the matching document
1966
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
1967
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
1968
+ // Extract originalToTrackerCustomerMapping if it exists
1969
+ const mapping =
1970
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
1971
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
1972
+ {};
1973
+
1974
+ // Find tempIds that exist in both revopInfo results and manager mapping
1975
+ const temp = [];
1976
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
1977
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
1978
+ if ( isSendMessge == true ) {
1979
+ logger.info( '....1' );
1980
+ }
1981
+ }
1982
+ return res.sendSuccess( 'Ticket closed successfully' );
1983
+ } else {
1984
+ return res.sendError( 'Internal Server Error', 500 );
1985
+ }
1986
+ } catch ( error ) {
1987
+ const err = error.message || 'Internal Server Error';
1988
+ logger.error( { error: err, funtion: 'ticketCreation' } );
1989
+ return res.sendError( err, 500 );
1990
+ }
1991
+ }
1992
+
1993
+ export async function getAssinedStore( user, storeId ) {
1994
+ if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
1995
+ return;
1996
+ }
1997
+
1998
+ const clientId = user.clientId;
1999
+ const storeIds = new Set(
2000
+ user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
2001
+ );
2002
+
2003
+ const addClusterStores = ( clusters ) => {
2004
+ if ( !clusters?.length ) return;
2005
+ for ( const cluster of clusters ) {
2006
+ cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
2007
+ }
2008
+ };
2009
+
2010
+ // Fetch all top-level data in parallel
2011
+ const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
2012
+ findcluster( {
2013
+ clientId,
2014
+ Teamlead: { $elemMatch: { email: user.email } },
2015
+ } ),
2016
+ findteams( {
2017
+ clientId,
2018
+ Teamlead: { $elemMatch: { email: user.email } },
2019
+ } ),
2020
+ findteams( {
2021
+ clientId,
2022
+ users: { $elemMatch: { email: user.email } },
2023
+ } ),
2024
+ ] );
2025
+
2026
+ // 1) Clusters where this user is Teamlead
2027
+ addClusterStores( clustersList );
2028
+
2029
+ // 2) Teams where this user is Teamlead → their users + their clusters
2030
+ if ( teamsList?.length ) {
2031
+ for ( const team of teamsList ) {
2032
+ if ( !team.users?.length ) continue;
2033
+
2034
+ await Promise.all(
2035
+ team.users.map( async ( teamUser ) => {
2036
+ const foundUser = await findOneUser( { _id: teamUser.userId } );
2037
+ if ( !foundUser ) return;
2038
+
2039
+ // Direct assigned stores of that user
2040
+ if ( foundUser.assignedStores?.length ) {
2041
+ foundUser.assignedStores.forEach( ( store ) =>
2042
+ storeIds.add( store.storeId ),
2043
+ );
2044
+ }
2045
+
2046
+ // Clusters where this user is Teamlead
2047
+ const userClustersList = await findcluster( {
2048
+ clientId,
2049
+ Teamlead: { $elemMatch: { email: foundUser.email } },
2050
+ } );
2051
+ addClusterStores( userClustersList );
2052
+ } ),
2053
+ );
2054
+ }
2055
+ }
2056
+
2057
+ // 3) Teams where this user is a member → clusters by teamName
2058
+ if ( teamMemberList?.length ) {
2059
+ for ( const team of teamMemberList ) {
2060
+ const clusterList = await findcluster( {
2061
+ clientId,
2062
+ teams: { $elemMatch: { name: team.teamName } },
2063
+ } );
2064
+ addClusterStores( clusterList );
2065
+ }
2066
+ }
2067
+
2068
+ const assignedStores = Array.from( storeIds );
2069
+
2070
+ // Previously you returned `true` in both branches.
2071
+ // Assuming you actually want to check membership:
2072
+ return assignedStores.includes( storeId );
2073
+ }
678
2074