tango-app-api-infra 3.9.5-vms.9 → 3.9.5-vms.91

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, getOpenSearchById } 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
- import { findOneClient } from '../services/client.service.js';
5
+ import { aggregateClient, 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,16 +277,90 @@ 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 configQuery = [
281
+ {
282
+ $match: {
283
+ clientId: getstoreName?.clientId,
284
+ },
285
+ },
286
+
287
+ // Convert all effectiveFrom to proper Date
288
+ {
289
+ $addFields: {
290
+ taggingLimitationWithDate: {
291
+ $map: {
292
+ input: '$footfallDirectoryConfigs.taggingLimitation',
293
+ as: 'item',
294
+ in: {
295
+ effectiveFrom: { $toDate: '$$item.effectiveFrom' },
296
+ values: '$$item.values',
297
+ },
298
+ },
299
+ },
300
+ },
301
+ },
302
+
303
+ // Filter items <= input date
304
+ {
305
+ $addFields: {
306
+ matchedLimitation: {
307
+ $filter: {
308
+ input: '$taggingLimitationWithDate',
309
+ as: 'item',
310
+ cond: {
311
+ $lte: [
312
+ '$$item.effectiveFrom',
313
+ { $toDate: inputData.dateString },
314
+ ],
315
+ },
316
+ },
317
+ },
318
+ },
319
+ },
320
+
321
+ // Sort DESC and pick ONLY top 1 -> latest effective record
322
+ {
323
+ $addFields: {
324
+ effectiveLimitation: {
325
+ $arrayElemAt: [
326
+ {
327
+ $slice: [
328
+ {
329
+ $sortArray: {
330
+ input: '$matchedLimitation',
331
+ sortBy: { effectiveFrom: -1 },
332
+ },
333
+ },
334
+ 1,
335
+ ],
336
+ },
337
+ 0,
338
+ ],
339
+ },
340
+ },
341
+ },
342
+
343
+ {
344
+ $project: {
345
+ config: 1,
346
+ effectiveLimitation: 1,
347
+ footfallDirectoryConfigs: 1,
348
+ },
349
+ },
350
+ ];
351
+
352
+
353
+ const config = await aggregateClient( configQuery );
354
+ const getConfig = config[0];
269
355
  if ( !getConfig || getConfig == null ) {
270
356
  return res.sendError( 'The Client ID is either not configured or not found', 400 );
271
357
  }
272
358
 
273
359
  // Get taggingLimitation from config (check both possible paths)
274
- const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
360
+ const taggingLimitation = getConfig?.effectiveLimitation?.values;
275
361
  // Initialize count object from taggingLimitation
276
362
  const tempAcc = [];
277
- const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
363
+ taggingLimitation.reduce( ( acc, item ) => {
278
364
  if ( item?.type ) {
279
365
  // Convert type to camelCase with "Count" suffix
280
366
  // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
@@ -318,6 +404,16 @@ export async function ticketCreation( req, res, next ) {
318
404
  'dateString': inputData.dateString,
319
405
  },
320
406
  },
407
+ {
408
+ term: {
409
+ 'isParent': false,
410
+ },
411
+ },
412
+ {
413
+ term: {
414
+ isChecked: true,
415
+ },
416
+ },
321
417
  ],
322
418
  },
323
419
  },
@@ -362,6 +458,7 @@ export async function ticketCreation( req, res, next ) {
362
458
  const totalCount = Array.isArray( tempAcc ) ?
363
459
  tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
364
460
  0;
461
+
365
462
  const footfallCount = hits?.[0]?._source?.footfall_count || 0;
366
463
  const revisedFootfall = Math.max( 0, footfallCount - totalCount );
367
464
  if ( footfallCount - revisedFootfall == 0 ) {
@@ -400,24 +497,27 @@ export async function ticketCreation( req, res, next ) {
400
497
  type: 'store',
401
498
  dateString: inputData.dateString,
402
499
  storeName: getstoreName?.storeName,
403
- ticketName: inputData.ticketName|| 'footfall-directory',
500
+ ticketName: inputData.ticketName || 'footfall-directory',
404
501
  footfallCount: footfallCount,
405
502
  clientId: getstoreName?.clientId,
406
503
  ticketId: 'TE_FDT_' + new Date().valueOf(),
407
504
  createdAt: new Date(),
408
505
  updatedAt: new Date(),
409
- status: 'raised',
506
+ status: 'Raised',
507
+ comments: inputData?.comments || '',
410
508
  revicedFootfall: revisedFootfall,
411
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
509
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
510
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
412
511
  mappingInfo: [
413
512
  {
414
513
  type: 'tagging',
415
514
  mode: inputData.mode,
416
515
  revicedFootfall: revisedFootfall,
417
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
516
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
517
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
418
518
  count: tempAcc,
419
519
  revisedDetail: formattedTaggingData,
420
- status: 'raised',
520
+ status: 'Raised',
421
521
  createdByEmail: req?.user?.email,
422
522
  createdByUserName: req?.user?.userName,
423
523
  createdByRole: req?.user?.role,
@@ -428,89 +528,92 @@ export async function ticketCreation( req, res, next ) {
428
528
 
429
529
 
430
530
  // Retrieve client footfallDirectoryConfigs revision
431
- let isAutoCloseEnable = false;
432
- let autoCloseAccuracy = '95%';
433
- try {
434
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
435
- if ( clientData?.footfallDirectoryConfigs ) {
436
- isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
437
- autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
438
- }
439
- } catch ( e ) {
440
- isAutoCloseEnable = false;
441
- autoCloseAccuracy = '95%';
442
- }
531
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
532
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
443
533
 
444
- let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || '95' ).replace( '%', '' ) );
445
- let revisedPercentage = 0;
446
- if ( typeof getCategory === 'number' && getCategory > 0 ) {
447
- revisedPercentage = ( revisedFootfall / getCategory ) * 100;
448
- }
534
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
535
+
536
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
537
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
449
538
 
450
539
  // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
451
540
  if (
452
541
  isAutoCloseEnable === true &&
453
542
  revisedPercentage >= autoCloseAccuracyValue
454
543
  ) {
455
- record.status = 'closed';
544
+ record.status = 'Closed';
456
545
  record.mappingInfo = [
457
546
  {
458
547
  type: 'tagging',
459
548
  mode: inputData.mode,
460
549
  revicedFootfall: revisedFootfall,
461
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
462
- count: getCategory,
550
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
551
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
552
+ count: tempAcc,
553
+ revisedDetail: formattedTaggingData,
554
+ status: 'Closed',
555
+ createdByEmail: req?.user?.email,
556
+ createdByUserName: req?.user?.userName,
557
+ createdByRole: req?.user?.role,
558
+ createdAt: new Date(),
559
+ },
560
+ {
561
+ type: 'finalRevision',
562
+ mode: inputData.mode,
563
+ revicedFootfall: revisedFootfall,
564
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
565
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
566
+ count: tempAcc,
463
567
  revisedDetail: formattedTaggingData,
464
- status: 'closed',
568
+ status: 'Closed',
465
569
  createdByEmail: req?.user?.email,
466
570
  createdByUserName: req?.user?.userName,
467
571
  createdByRole: req?.user?.role,
572
+ createdAt: new Date(),
468
573
  },
469
574
  ];
470
575
  } else {
471
- // If ticket is closed, do not proceed with revision mapping
576
+ // If ticket is closed, do not proceed with revision mapping
472
577
  let revisionArray = [];
473
- try {
474
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
475
- revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
476
- } catch ( e ) {
477
- revisionArray = [];
478
- }
479
578
 
579
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
480
580
  // Default fallbacks
481
581
  let revisionMapping = null;
482
582
  let approverMapping = null;
483
583
  let tangoReviewMapping = null;
484
-
485
584
  // Find out which roles have isChecked true
486
585
  if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
487
586
  for ( const r of revisionArray ) {
488
587
  if ( r.actionType === 'reviewer' && r.isChecked === true ) {
489
588
  revisionMapping = {
490
589
  type: 'review',
491
- revicedFootfall: revisedFootfall,
492
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
493
- count: getCategory,
590
+ // revicedFootfall: revisedFootfall,
591
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
592
+ count: tempAcc,
494
593
  revisedDetail: formattedTaggingData,
495
- status: 'open',
594
+ status: 'Open',
595
+ dueDate: new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
596
+
496
597
  };
497
598
  } else if ( r.actionType === 'approver' && r.isChecked === true ) {
498
599
  approverMapping = {
499
- type: 'approver',
500
- revicedFootfall: revisedFootfall,
501
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
502
- count: getCategory,
600
+ type: 'approve',
601
+ // revicedFootfall: revisedFootfall,
602
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
603
+ count: tempAcc,
503
604
  revisedDetail: formattedTaggingData,
504
- status: 'open',
605
+ status: 'Open',
606
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
505
607
  };
506
608
  } else if ( r.actionType === 'tango' && r.isChecked === true ) {
507
609
  tangoReviewMapping = {
508
- type: 'tango-review',
509
- revicedFootfall: revisedFootfall,
510
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
511
- count: getCategory,
610
+ type: 'tangoreview',
611
+ // revicedFootfall: revisedFootfall,
612
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
613
+ count: tempAcc,
512
614
  revisedDetail: formattedTaggingData,
513
- status: 'open',
615
+ status: 'Open',
616
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
514
617
  };
515
618
  }
516
619
  }
@@ -518,36 +621,179 @@ export async function ticketCreation( req, res, next ) {
518
621
 
519
622
  // Insert appropriate mappingInfo blocks
520
623
  if ( revisionMapping ) {
521
- // If reviewer and checked
624
+ // If reviewer and checked
522
625
  record.mappingInfo.push( revisionMapping );
523
626
  } else if ( approverMapping ) {
524
- // If approver and checked
627
+ // If approver and checked
525
628
  record.mappingInfo.push( approverMapping );
526
629
  } else if ( tangoReviewMapping ) {
527
- // If none above, then tangoReview
630
+ // If none above, then tangoReview
528
631
  record.mappingInfo.push( tangoReviewMapping );
529
632
  }
530
633
  }
531
634
 
635
+ const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
636
+
637
+ const hasReviewer = revision.some(
638
+ ( data ) => data.actionType === 'reviewer' && data.isChecked === true,
639
+ );
640
+ const hasApprover = revision.some(
641
+ ( data ) => data.actionType === 'approver' && data.isChecked === true,
642
+ );
643
+
644
+ if ( hasReviewer || hasApprover ) {
645
+ const userQuery = [
646
+ {
647
+ $match: {
648
+ clientId: getstoreName.clientId,
649
+ role: 'admin',
650
+ isActive: true,
651
+ },
652
+ },
653
+ ];
654
+
655
+ const finduserList = await aggregateUser( userQuery );
656
+
657
+
658
+ const createdOn = dayjs().format( 'DD MMM YYYY' );
659
+ const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
660
+ const description = `Created on ${createdOn}`;
661
+
662
+ const Data = {
663
+ title,
664
+ body: description,
665
+ type: 'create',
666
+ date: record.dateString,
667
+ storeId: record.storeId,
668
+ clientId: record.clientId,
669
+ ticketId: record.ticketId,
670
+ };
671
+
672
+ await Promise.all(
673
+ ( finduserList || [] ).map( async ( userData ) => {
674
+ const ticketsFeature = userData?.rolespermission?.some(
675
+ ( f ) =>
676
+ f.featureName === 'FootfallDirectory' &&
677
+ f.modules?.some(
678
+ ( m ) =>
679
+ m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
680
+ ),
681
+ );
682
+
683
+
684
+ if ( !ticketsFeature ) return;
685
+
686
+ const notifyUser = await getAssinedStore( userData, req.body.storeId );
687
+ if ( !notifyUser || !userData?.fcmToken ) return;
688
+
689
+ await sendPushNotification( title, description, userData.fcmToken, Data );
690
+ } ),
691
+ );
692
+ }
693
+
532
694
 
533
695
  const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
534
696
  const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
535
697
  if ( insertResult && insertResult.statusCode === 201 ) {
536
698
  // After successful ticket creation, update status to "submitted" in revop index for the relevant records
537
- try {
538
- const bulkUpdateBody = taggingImages.map( ( img ) => [
539
- { update: { _index: openSearch.revop, _id: img._id } },
540
- { doc: { status: 'submitted' } },
541
- ] ).flat();
542
-
543
- if ( bulkUpdateBody.length > 0 ) {
544
- await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
545
- }
546
- } catch ( updateErr ) {
547
- logger.error( { error: updateErr, message: 'Failed to update status to submitted in revop index' } );
548
- // Do not block the success response for this failure
699
+
700
+
701
+ const bulkUpdateBody = taggingImages.map( ( img ) => [
702
+ { update: { _index: openSearch.revop, _id: img._id } },
703
+ { doc: { status: 'submitted' } },
704
+ ] ).flat();
705
+
706
+ if ( bulkUpdateBody.length > 0 ) {
707
+ await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
549
708
  }
550
709
 
710
+ if ( record.status = 'Closed' ) {
711
+ const query = {
712
+ storeId: inputData?.storeId,
713
+ isVideoStream: true,
714
+ };
715
+ const getStoreType = await countDocumnetsCamera( query );
716
+ const revopInfoQuery = {
717
+ size: 10000,
718
+ query: {
719
+ bool: {
720
+ must: [
721
+ {
722
+ term: {
723
+ 'storeId.keyword': inputData.storeId,
724
+ },
725
+ },
726
+ {
727
+ term: {
728
+ 'dateString': inputData.dateString,
729
+ },
730
+ },
731
+ {
732
+ term: {
733
+ 'isParent': false,
734
+ },
735
+ },
736
+ {
737
+ term: {
738
+ isChecked: true,
739
+ },
740
+ },
741
+ ],
742
+ },
743
+ },
744
+ _source: [ 'tempId' ],
745
+
746
+ };
747
+
748
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
749
+
750
+ // Get all tempIds from revopInfo response
751
+ const tempIds =
752
+ revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
753
+ // Prepare management eyeZone query based on storeId and dateString
754
+ const managerEyeZoneQuery = {
755
+ size: 1,
756
+ query: {
757
+ bool: {
758
+ must: [
759
+ {
760
+ term: {
761
+ 'storeId.keyword': inputData.storeId,
762
+ },
763
+ },
764
+ {
765
+ term: {
766
+ 'storeDate': inputData.dateString,
767
+ },
768
+ },
769
+ ],
770
+ },
771
+ },
772
+ _source: [ 'originalToTrackerCustomerMapping' ],
773
+ };
774
+
775
+ // Query the managerEyeZone index for the matching document
776
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
777
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
778
+ // Extract originalToTrackerCustomerMapping if it exists
779
+ const mapping =
780
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
781
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
782
+ {};
783
+
784
+ // If you want to compare or find matching tempIds in the mapping
785
+ // The mapping is { "1": tempId1, ... }, so get values as array of tempIds
786
+ // const managerMappedTempIds = Object.values( mapping );
787
+
788
+ // Find tempIds that exist in both revopInfo results and manager mapping
789
+ const temp = [];
790
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
791
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
792
+ if ( isSendMessge == true ) {
793
+ logger.info( '....1' );
794
+ // return true; // res.sendSuccess( 'Ticket has been updated successfully' );
795
+ } // Example: log or use these tempIds for further logic
796
+ }
551
797
  // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
552
798
 
553
799
  if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
@@ -557,7 +803,7 @@ export async function ticketCreation( req, res, next ) {
557
803
  // req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
558
804
  const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
559
805
  const storeId = inputData.storeId;
560
- const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
806
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
561
807
 
562
808
 
563
809
  const formatDate = ( d ) =>
@@ -671,14 +917,1481 @@ export async function ticketCreation( req, res, next ) {
671
917
  }
672
918
  }
673
919
  }
920
+ const sqsName = sqs.vmsPickleExtention;
921
+ const sqsProduceQueue = {
922
+ QueueUrl: `${sqs.url}${sqsName}`,
923
+ MessageBody: JSON.stringify( {
924
+ store_id: inputData?.storeId,
925
+ store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
926
+ primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
927
+ time: new Date(),
928
+ } ),
929
+ MessageGroupId: 'revops-pickle',
930
+ MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
931
+ };
932
+ const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
933
+
934
+ if ( sqsQueue.statusCode ) {
935
+ logger.error( {
936
+ error: `${sqsQueue}`,
937
+ type: 'SQS_NOT_SEND_ERROR',
938
+ } );
939
+ }
940
+
674
941
 
675
942
  return res.sendSuccess( 'Ticket raised successfully' );
676
943
  }
677
944
  } catch ( error ) {
678
945
  const err = error.message || 'Internal Server Error';
679
- logger.error( { error: err, funtion: 'ticketCreation' } );
946
+ logger.error( { error: error, funtion: 'ticketCreation' } );
680
947
  return res.sendError( err, 500 );
681
948
  }
682
949
  }
683
950
 
951
+ export async function ticketReview( req, res, next ) {
952
+ try {
953
+ const inputData = req.body;
954
+ if ( inputData?.type !== 'review' ) {
955
+ return next();
956
+ }
957
+ // check the createtion permission from the user permission
958
+ const userInfo = req?.user;
959
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
960
+ logger.info( { ticketsFeature, userInfo } );
961
+ if ( !ticketsFeature ) {
962
+ return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
963
+ }
964
+
965
+ // get store info by the storeId into mongo db
966
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
967
+ logger.info( { getstoreName } );
968
+ if ( !getstoreName || getstoreName == null ) {
969
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
970
+ }
971
+
972
+ // get the footfall count from opensearch
973
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
974
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
975
+ const getQuery = {
976
+ query: {
977
+ terms: {
978
+ _id: [ dateString ],
979
+ },
980
+ },
981
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
982
+ sort: [
983
+ {
984
+ date_iso: {
985
+ order: 'desc',
986
+ },
987
+ },
988
+ ],
989
+ };
990
+
991
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
992
+ const hits = getFootfallCount?.body?.hits?.hits || [];
993
+ logger.info( { hits } );
994
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
995
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
996
+ }
997
+
998
+ // get category details from the client level configuration
999
+ const configQuery = [
1000
+ {
1001
+ $match: {
1002
+ clientId: getstoreName?.clientId,
1003
+ },
1004
+ },
1005
+
1006
+ // Convert all effectiveFrom to proper Date
1007
+ {
1008
+ $addFields: {
1009
+ taggingLimitationWithDate: {
1010
+ $map: {
1011
+ input: '$footfallDirectoryConfigs.taggingLimitation',
1012
+ as: 'item',
1013
+ in: {
1014
+ effectiveFrom: { $toDate: '$$item.effectiveFrom' },
1015
+ values: '$$item.values',
1016
+ },
1017
+ },
1018
+ },
1019
+ },
1020
+ },
1021
+
1022
+ // Filter items <= input date
1023
+ {
1024
+ $addFields: {
1025
+ matchedLimitation: {
1026
+ $filter: {
1027
+ input: '$taggingLimitationWithDate',
1028
+ as: 'item',
1029
+ cond: {
1030
+ $lte: [
1031
+ '$$item.effectiveFrom',
1032
+ { $toDate: inputData.dateString },
1033
+ ],
1034
+ },
1035
+ },
1036
+ },
1037
+ },
1038
+ },
1039
+
1040
+ // Sort DESC and pick ONLY top 1 -> latest effective record
1041
+ {
1042
+ $addFields: {
1043
+ effectiveLimitation: {
1044
+ $arrayElemAt: [
1045
+ {
1046
+ $slice: [
1047
+ {
1048
+ $sortArray: {
1049
+ input: '$matchedLimitation',
1050
+ sortBy: { effectiveFrom: -1 },
1051
+ },
1052
+ },
1053
+ 1,
1054
+ ],
1055
+ },
1056
+ 0,
1057
+ ],
1058
+ },
1059
+ },
1060
+ },
1061
+
1062
+ {
1063
+ $project: {
1064
+ config: 1,
1065
+ effectiveLimitation: 1,
1066
+ footfallDirectoryConfigs: 1,
1067
+ },
1068
+ },
1069
+ ];
1070
+
1071
+
1072
+ const config = await aggregateClient( configQuery );
1073
+ const getConfig = config[0];
1074
+ if ( !getConfig || getConfig == null ) {
1075
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
1076
+ }
1077
+
1078
+ logger.info( { config } );
1079
+
1080
+ // Get taggingLimitation from config (check both possible paths)
1081
+ const taggingLimitation = getConfig?.effectiveLimitation?.values;
1082
+ // Initialize count object from taggingLimitation
1083
+ const tempAcc = [];
1084
+ taggingLimitation.reduce( ( acc, item ) => {
1085
+ if ( item?.type ) {
1086
+ // Convert type to camelCase with "Count" suffix
1087
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
1088
+ const typeLower = item.type.toLowerCase();
1089
+ let key;
1090
+ if ( typeLower === 'housekeeping' ) {
1091
+ key = 'houseKeepingCount';
1092
+ } else {
1093
+ // Convert first letter to lowercase and append "Count"
1094
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
1095
+ }
1096
+
1097
+
1098
+ // To change from an object to the desired array structure, assemble an array of objects:
1099
+ tempAcc.push( {
1100
+ name: item.name,
1101
+ value: 0,
1102
+ key: key,
1103
+ type: item.type,
1104
+ } );
1105
+
1106
+
1107
+ return acc;
1108
+ }
1109
+ }, {} ) || {};
1110
+
1111
+ // Query OpenSearch revop index to get actual counts for each type
1112
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
1113
+ const revopQuery = {
1114
+ size: 0,
1115
+ query: {
1116
+ bool: {
1117
+ must: [
1118
+ {
1119
+ term: {
1120
+ 'storeId.keyword': inputData.storeId,
1121
+ },
1122
+ },
1123
+ {
1124
+ term: {
1125
+ 'dateString': inputData.dateString,
1126
+ },
1127
+ },
1128
+ {
1129
+ term: {
1130
+ 'isParent': false,
1131
+ },
1132
+ },
1133
+ {
1134
+ term: {
1135
+ isChecked: true,
1136
+ },
1137
+ },
1138
+ ],
1139
+ },
1140
+ },
1141
+ aggs: {
1142
+ type_counts: {
1143
+ terms: {
1144
+ field: 'revopsType.keyword',
1145
+ size: 100,
1146
+ },
1147
+ },
1148
+ },
1149
+ };
1150
+
1151
+
1152
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1153
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1154
+ logger.info( { revopData } );
1155
+ // Map OpenSearch revopsType values to count object keys
1156
+ buckets.forEach( ( bucket ) => {
1157
+ const revopsType = bucket.key;
1158
+ const count = bucket.doc_count || 0;
1159
+
1160
+
1161
+ if ( Array.isArray( tempAcc ) ) {
1162
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1163
+ const accMatch = tempAcc.find(
1164
+ ( acc ) =>
1165
+ acc.type &&
1166
+ acc.type === revopsType,
1167
+ );
1168
+
1169
+ if ( accMatch && accMatch.key ) {
1170
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1171
+ }
1172
+ }
1173
+ } );
1174
+ }
1175
+
1176
+
1177
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1178
+
1179
+ const totalCount = Array.isArray( tempAcc ) ?
1180
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1181
+ 0;
1182
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1183
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1184
+ if ( footfallCount - revisedFootfall == 0 ) {
1185
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1186
+ }
1187
+ logger.info( { footfallCount, revisedFootfall } );
1188
+ const taggingData = {
1189
+ size: 10000,
1190
+ query: {
1191
+ bool: {
1192
+ must: [
1193
+ {
1194
+ term: {
1195
+ 'storeId.keyword': inputData.storeId,
1196
+ },
1197
+ },
1198
+ {
1199
+ term: {
1200
+ 'dateString': inputData.dateString,
1201
+ },
1202
+ },
1203
+ ],
1204
+ },
1205
+ },
1206
+ };
1207
+
1208
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1209
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1210
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1211
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1212
+ }
1213
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1214
+
1215
+ const getTicket = {
1216
+ size: 10000,
1217
+ query: {
1218
+ bool: {
1219
+ must: [
1220
+ {
1221
+ term: {
1222
+ 'type.keyword': 'store',
1223
+ },
1224
+ },
1225
+ {
1226
+ term: {
1227
+ 'type.keyword': 'store',
1228
+ },
1229
+ },
1230
+ {
1231
+ term: {
1232
+ 'storeId.keyword': inputData.storeId,
1233
+ },
1234
+ },
1235
+ {
1236
+ term: {
1237
+ 'dateString': inputData.dateString,
1238
+ },
1239
+ },
1240
+ ],
1241
+ },
1242
+ },
1243
+ };
1244
+
1245
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1246
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1247
+ if ( !ticketData || ticketData?.length == 0 ) {
1248
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1249
+ }
1250
+ logger.info( { ticketData, mappingInfo: ticketData?.[0]?._source?.mappingInfo } );
1251
+ const record = {
1252
+
1253
+ status: 'Reviewer-Closed',
1254
+ revicedFootfall: revisedFootfall,
1255
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1256
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1257
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1258
+ // createdByEmail: req?.user?.email,
1259
+ // createdByUserName: req?.user?.userName,
1260
+ // createdByRole: req?.user?.role,
1261
+
1262
+ };
1263
+ logger.info( { record } );
1264
+ if ( Array.isArray( record.mappingInfo ) ) {
1265
+ const temp = record.mappingInfo
1266
+ .filter( ( item ) => item.type === 'review' )
1267
+ .map( ( item ) => ( {
1268
+ ...item,
1269
+ mode: inputData.mode,
1270
+ revicedFootfall: revisedFootfall,
1271
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1272
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1273
+ count: tempAcc,
1274
+ revisedDetail: formattedTaggingData,
1275
+ status: 'Closed',
1276
+ createdByEmail: req?.user?.email,
1277
+ createdByUserName: req?.user?.userName,
1278
+ createdByRole: req?.user?.role,
1279
+ createdAt: new Date(),
1280
+ } ) );
1281
+ record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
1282
+ // If no review mapping existed, push a new one
1283
+ if ( record.mappingInfo.length === 0 ) {
1284
+ record.mappingInfo.push( {
1285
+ type: 'review',
1286
+ mode: inputData.mode,
1287
+ revicedFootfall: revisedFootfall,
1288
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1289
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1290
+ count: tempAcc,
1291
+ revisedDetail: formattedTaggingData,
1292
+ status: 'Closed',
1293
+ createdByEmail: req?.user?.email,
1294
+ createdByUserName: req?.user?.userName,
1295
+ createdByRole: req?.user?.role,
1296
+ createdAt: new Date(),
1297
+ } );
1298
+ }
1299
+ }
1300
+
1301
+
1302
+ // Retrieve client footfallDirectoryConfigs revision
1303
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
1304
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1305
+ logger.info( { isAutoCloseEnable, autoCloseAccuracy } );
1306
+
1307
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1308
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1309
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1310
+
1311
+
1312
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1313
+ if (
1314
+ isAutoCloseEnable === true &&
1315
+ revisedPercentage >= autoCloseAccuracyValue
1316
+ ) {
1317
+ record.status = 'Reviewer-Closed';
1318
+ // Only keep or modify mappingInfo items with type "review"
1319
+ if ( Array.isArray( record.mappingInfo ) ) {
1320
+ const temp = record.mappingInfo
1321
+ .filter( ( item ) => item.type === 'review' )
1322
+ .map( ( item ) => ( {
1323
+ ...item,
1324
+ mode: inputData.mode,
1325
+ revicedFootfall: revisedFootfall,
1326
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1327
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1328
+ count: tempAcc,
1329
+ revisedDetail: formattedTaggingData,
1330
+ status: 'Closed',
1331
+ createdByEmail: req?.user?.email,
1332
+ createdByUserName: req?.user?.userName,
1333
+ createdByRole: req?.user?.role,
1334
+ } ) );
1335
+
1336
+ const temp2 = record.mappingInfo
1337
+ .filter( ( item ) => item.type === 'tagging' )
1338
+ .map( ( item ) => ( {
1339
+ ...item,
1340
+ mode: inputData.mode,
1341
+ // revicedFootfall: revisedFootfall,
1342
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1343
+ // count: tempAcc,
1344
+ // revisedDetail: formattedTaggingData,
1345
+ status: 'Closed',
1346
+ // createdByEmail: req?.user?.email,
1347
+ // createdByUserName: req?.user?.userName,
1348
+ // createdByRole: req?.user?.role,
1349
+ } ) );
1350
+ record.mappingInfo = [ ...temp2, ...temp ];
1351
+ // If no review mapping existed, push a new one
1352
+ if ( record.mappingInfo.length === 0 ) {
1353
+ record.mappingInfo.push( {
1354
+ type: 'review',
1355
+ mode: inputData.mode,
1356
+ revicedFootfall: revisedFootfall,
1357
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1358
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1359
+ count: tempAcc,
1360
+ revisedDetail: formattedTaggingData,
1361
+ status: 'Closed',
1362
+ createdByEmail: req?.user?.email,
1363
+ createdByUserName: req?.user?.userName,
1364
+ createdByRole: req?.user?.role,
1365
+ } );
1366
+ }
1367
+ }
1368
+ record.mappingInfo.push(
1369
+ {
1370
+ type: 'finalRevision',
1371
+ mode: inputData.mode,
1372
+ revicedFootfall: revisedFootfall,
1373
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1374
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1375
+ count: tempAcc,
1376
+ revisedDetail: formattedTaggingData,
1377
+ status: 'Closed',
1378
+ createdByEmail: req?.user?.email,
1379
+ createdByUserName: req?.user?.userName,
1380
+ createdByRole: req?.user?.role,
1381
+ createdAt: new Date(),
1382
+ },
1383
+ );
1384
+ logger.info( { revisedPercentage } );
1385
+ } else {
1386
+ // If ticket is closed, do not proceed with revision mapping
1387
+ let revisionArray = [];
1388
+
1389
+
1390
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
1391
+ logger.info( { revisionArray } );
1392
+
1393
+ // Default fallbacks
1394
+ let revisionMapping = null;
1395
+ let approverMapping = null;
1396
+ let tangoReviewMapping = null;
1397
+
1398
+ // Find out which roles have isChecked true
1399
+ if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
1400
+ for ( const r of revisionArray ) {
1401
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
1402
+ approverMapping = {
1403
+ type: 'approve',
1404
+ // revicedFootfall: revisedFootfall,
1405
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1406
+ count: tempAcc,
1407
+ revisedDetail: formattedTaggingData,
1408
+ status: 'Open',
1409
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
1410
+ };
1411
+ } else if ( r.actionType === 'tango' && r.isChecked === true ) {
1412
+ tangoReviewMapping = {
1413
+ type: 'tangoreview',
1414
+ // revicedFootfall: revisedFootfall,
1415
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1416
+ count: tempAcc,
1417
+ revisedDetail: formattedTaggingData,
1418
+ status: 'Open',
1419
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
1420
+ };
1421
+ }
1422
+ }
1423
+ }
1424
+ logger.info( { record } );
1425
+ // Insert appropriate mappingInfo blocks
1426
+ if ( revisionMapping ) {
1427
+ // If reviewer and checked
1428
+ record.mappingInfo.push( revisionMapping );
1429
+ } else if ( approverMapping ) {
1430
+ // If approver and checked
1431
+ record.mappingInfo.push( approverMapping );
1432
+ } else if ( tangoReviewMapping ) {
1433
+ // If none above, then tangoReview
1434
+ record.mappingInfo.push( tangoReviewMapping );
1435
+ }
1436
+ }
1437
+
1438
+ let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
1439
+
1440
+
1441
+ if ( checkreview.length > 0&&record.status!='Reviewer-Closed' ) {
1442
+ let userQuery = [
1443
+ {
1444
+ $match: {
1445
+ clientId: getstoreName.clientId,
1446
+ role: 'admin',
1447
+ isActive: true,
1448
+ },
1449
+ },
1450
+ ];
1451
+ let finduserList = await aggregateUser( userQuery );
1452
+
1453
+
1454
+ // return;
1455
+ for ( let userData of finduserList ) {
1456
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1457
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1458
+ let description = `Created on ${createdOn}`;
1459
+
1460
+
1461
+ let Data = {
1462
+ 'title': title,
1463
+ 'body': description,
1464
+ 'type': 'review',
1465
+ 'date': record.dateString,
1466
+ 'storeId': record.storeId,
1467
+ 'clientId': record.clientId,
1468
+ 'ticketId': record.ticketId,
1469
+ };
1470
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1471
+
1472
+ if ( ticketsFeature ) {
1473
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1474
+ if ( userData && userData.fcmToken && notifyuser ) {
1475
+ const fcmToken = userData.fcmToken;
1476
+ await sendPushNotification( title, description, fcmToken, Data );
1477
+ }
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1483
+ console.log( '-----', record.status );
1484
+ if ( record.status==='Reviewer-Closed' ) {
1485
+ console.log( '🚀 ~ ticketReview ~ id:', id );
1486
+ let Ticket = await getOpenSearchById( openSearch.footfallDirectory, id );
1487
+ console.log( '🚀 ~ ticketReview ~ Ticket:', Ticket.body );
1488
+ if ( Ticket?.body?._source?.type==='store' ) {
1489
+ let findTagging = Ticket?.body?._source?.mappingInfo.filter( ( data ) => data.type==='tagging' );
1490
+ if ( findTagging?.length>0&&findTagging[0].createdByEmail!='' ) {
1491
+ let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
1492
+ console.log( '🚀 ~ ticketReview ~ findTagging[0]?.createdByEmail:', findTagging[0]?.createdByEmail );
1493
+ let title = `Received response for the Footfall ticket raised.`;
1494
+ let createdOn = dayjs( Ticket?.body?._source?.dateString ).format( 'DD MMM YYYY' );
1495
+ let description = `Raised on ${createdOn}`;
1496
+
1497
+ let Data = {
1498
+ 'title': title,
1499
+ 'body': description,
1500
+ 'type': 'closed',
1501
+ 'date': Ticket?.body?._source?.dateString,
1502
+ 'storeId': Ticket?.body?._source?.storeId,
1503
+ 'clientId': Ticket?.body?._source?.clientId,
1504
+ 'ticketId': Ticket?.body?._source?.ticketId,
1505
+ };
1506
+ if ( userData && userData.fcmToken ) {
1507
+ const fcmToken = userData.fcmToken;
1508
+ await sendPushNotification( title, description, fcmToken, Data );
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1514
+
1515
+ if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
1516
+ if ( record.status = 'Closed' ) {
1517
+ const query = {
1518
+ storeId: inputData?.storeId,
1519
+ isVideoStream: true,
1520
+ };
1521
+ const getStoreType = await countDocumnetsCamera( query );
1522
+ const revopInfoQuery = {
1523
+ size: 10000,
1524
+ query: {
1525
+ bool: {
1526
+ must: [
1527
+ {
1528
+ term: {
1529
+ 'storeId.keyword': inputData.storeId,
1530
+ },
1531
+ },
1532
+ {
1533
+ term: {
1534
+ 'dateString': inputData.dateString,
1535
+ },
1536
+ },
1537
+ {
1538
+ term: {
1539
+ 'isParent': false,
1540
+ },
1541
+ },
1542
+ {
1543
+ term: {
1544
+ isChecked: true,
1545
+ },
1546
+ },
1547
+ ],
1548
+ },
1549
+ },
1550
+ _source: [ 'tempId' ],
1551
+
1552
+ };
1553
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
1554
+ // Get all tempIds from revopInfo response
1555
+ const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
1556
+ // Prepare management eyeZone query based on storeId and dateString
1557
+ const managerEyeZoneQuery = {
1558
+ size: 1,
1559
+ query: {
1560
+ bool: {
1561
+ must: [
1562
+ {
1563
+ term: {
1564
+ 'storeId.keyword': inputData.storeId,
1565
+ },
1566
+ },
1567
+ {
1568
+ term: {
1569
+ 'storeDate': inputData.dateString,
1570
+ },
1571
+ },
1572
+ ],
1573
+ },
1574
+ },
1575
+ _source: [ 'originalToTrackerCustomerMapping' ],
1576
+ };
1577
+
1578
+ // Query the managerEyeZone index for the matching document
1579
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
1580
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
1581
+ // Extract originalToTrackerCustomerMapping if it exists
1582
+ const mapping =
1583
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
1584
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
1585
+ {};
1586
+
1587
+ // Find tempIds that exist in both revopInfo results and manager mapping
1588
+ const temp = [];
1589
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
1590
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
1591
+ if ( isSendMessge == true ) {
1592
+ logger.info( '....1' );
1593
+ }
1594
+ }
1595
+ return res.sendSuccess( 'Ticket closed successfully' );
1596
+ } else {
1597
+ return res.sendError( 'Internal Server Error', 500 );
1598
+ }
1599
+ } catch ( error ) {
1600
+ const err = error.message || 'Internal Server Error';
1601
+ logger.error( { error: err, funtion: 'ticketreview' } );
1602
+ return res.sendError( error, 500 );
1603
+ }
1604
+ }
1605
+
1606
+ export async function ticketApprove( req, res, next ) {
1607
+ try {
1608
+ const inputData = req.body;
1609
+ if ( inputData?.type !== 'approve' ) {
1610
+ return next();
1611
+ }
1612
+ // check the createtion permission from the user permission
1613
+ const userInfo = req?.user;
1614
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1615
+ if ( !ticketsFeature ) {
1616
+ return res.sendError( 'Forbidden to Approve this Ticket', 403 );
1617
+ }
1618
+
1619
+ // get store info by the storeId into mongo db
1620
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
1621
+
1622
+ if ( !getstoreName || getstoreName == null ) {
1623
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
1624
+ }
1625
+
1626
+ // get the footfall count from opensearch
1627
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1628
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
1629
+ const getQuery = {
1630
+ query: {
1631
+ terms: {
1632
+ _id: [ dateString ],
1633
+ },
1634
+ },
1635
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
1636
+ sort: [
1637
+ {
1638
+ date_iso: {
1639
+ order: 'desc',
1640
+ },
1641
+ },
1642
+ ],
1643
+ };
1644
+
1645
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
1646
+ const hits = getFootfallCount?.body?.hits?.hits || [];
1647
+ logger.info( { hits } );
1648
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
1649
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
1650
+ }
1651
+
1652
+ // get category details from the client level configuration
1653
+ const configQuery = [
1654
+ {
1655
+ $match: {
1656
+ clientId: getstoreName?.clientId,
1657
+ },
1658
+ },
1659
+
1660
+ // Convert all effectiveFrom to proper Date
1661
+ {
1662
+ $addFields: {
1663
+ taggingLimitationWithDate: {
1664
+ $map: {
1665
+ input: '$footfallDirectoryConfigs.taggingLimitation',
1666
+ as: 'item',
1667
+ in: {
1668
+ effectiveFrom: { $toDate: '$$item.effectiveFrom' },
1669
+ values: '$$item.values',
1670
+ },
1671
+ },
1672
+ },
1673
+ },
1674
+ },
1675
+
1676
+ // Filter items <= input date
1677
+ {
1678
+ $addFields: {
1679
+ matchedLimitation: {
1680
+ $filter: {
1681
+ input: '$taggingLimitationWithDate',
1682
+ as: 'item',
1683
+ cond: {
1684
+ $lte: [
1685
+ '$$item.effectiveFrom',
1686
+ { $toDate: inputData.dateString },
1687
+ ],
1688
+ },
1689
+ },
1690
+ },
1691
+ },
1692
+ },
1693
+
1694
+ // Sort DESC and pick ONLY top 1 -> latest effective record
1695
+ {
1696
+ $addFields: {
1697
+ effectiveLimitation: {
1698
+ $arrayElemAt: [
1699
+ {
1700
+ $slice: [
1701
+ {
1702
+ $sortArray: {
1703
+ input: '$matchedLimitation',
1704
+ sortBy: { effectiveFrom: -1 },
1705
+ },
1706
+ },
1707
+ 1,
1708
+ ],
1709
+ },
1710
+ 0,
1711
+ ],
1712
+ },
1713
+ },
1714
+ },
1715
+
1716
+ {
1717
+ $project: {
1718
+ config: 1,
1719
+ effectiveLimitation: 1,
1720
+ footfallDirectoryConfigs: 1,
1721
+ },
1722
+ },
1723
+ ];
1724
+
1725
+
1726
+ const config = await aggregateClient( configQuery );
1727
+ const getConfig = config[0];
1728
+ if ( !getConfig || getConfig == null ) {
1729
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
1730
+ }
1731
+
1732
+ // Get taggingLimitation from config (check both possible paths)
1733
+ const taggingLimitation = getConfig?.effectiveLimitation?.values;
1734
+ // Initialize count object from taggingLimitation
1735
+ const tempAcc = [];
1736
+ taggingLimitation.reduce( ( acc, item ) => {
1737
+ if ( item?.type ) {
1738
+ // Convert type to camelCase with "Count" suffix
1739
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
1740
+ const typeLower = item.type.toLowerCase();
1741
+ let key;
1742
+ if ( typeLower === 'housekeeping' ) {
1743
+ key = 'houseKeepingCount';
1744
+ } else {
1745
+ // Convert first letter to lowercase and append "Count"
1746
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
1747
+ }
1748
+
1749
+
1750
+ // To change from an object to the desired array structure, assemble an array of objects:
1751
+ tempAcc.push( {
1752
+ name: item.name,
1753
+ value: 0,
1754
+ key: key,
1755
+ type: item.type,
1756
+ } );
1757
+
1758
+
1759
+ return acc;
1760
+ }
1761
+ }, {} ) || {};
1762
+
1763
+ // Query OpenSearch revop index to get actual counts for each type
1764
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
1765
+ const revopQuery = {
1766
+ size: 0,
1767
+ query: {
1768
+ bool: {
1769
+ must: [
1770
+ {
1771
+ term: {
1772
+ 'storeId.keyword': inputData.storeId,
1773
+ },
1774
+ },
1775
+ {
1776
+ term: {
1777
+ 'dateString': inputData.dateString,
1778
+ },
1779
+ },
1780
+ {
1781
+ term: {
1782
+ 'isParent': false,
1783
+ },
1784
+ },
1785
+ {
1786
+ term: {
1787
+ isChecked: true,
1788
+ },
1789
+ },
1790
+ ],
1791
+ },
1792
+ },
1793
+ aggs: {
1794
+ type_counts: {
1795
+ terms: {
1796
+ field: 'revopsType.keyword',
1797
+ size: 100,
1798
+ },
1799
+ },
1800
+ },
1801
+ };
1802
+
1803
+
1804
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1805
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1806
+
1807
+ // Map OpenSearch revopsType values to count object keys
1808
+ buckets.forEach( ( bucket ) => {
1809
+ const revopsType = bucket.key;
1810
+ const count = bucket.doc_count || 0;
1811
+
1812
+
1813
+ if ( Array.isArray( tempAcc ) ) {
1814
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1815
+ const accMatch = tempAcc.find(
1816
+ ( acc ) =>
1817
+ acc.type &&
1818
+ acc.type === revopsType,
1819
+ );
1820
+
1821
+ if ( accMatch && accMatch.key ) {
1822
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1823
+ }
1824
+ }
1825
+ } );
1826
+ }
1827
+
1828
+
1829
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1830
+
1831
+ const totalCount = Array.isArray( tempAcc ) ?
1832
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1833
+ 0;
1834
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1835
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1836
+
1837
+ if ( footfallCount - revisedFootfall == 0 ) {
1838
+ return res.sendError( 'Cannot approve a ticket because footfall hasn’t changed', 400 );
1839
+ }
1840
+
1841
+ const taggingData = {
1842
+ size: 10000,
1843
+ query: {
1844
+ bool: {
1845
+ must: [
1846
+ {
1847
+ term: {
1848
+ 'storeId.keyword': inputData.storeId,
1849
+ },
1850
+ },
1851
+ {
1852
+ term: {
1853
+ 'dateString': inputData.dateString,
1854
+ },
1855
+ },
1856
+ {
1857
+ term: {
1858
+ 'isParent': false,
1859
+ },
1860
+ },
1861
+ {
1862
+ term: {
1863
+ isChecked: true,
1864
+ },
1865
+ },
1866
+ ],
1867
+ },
1868
+ },
1869
+ };
1870
+
1871
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1872
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1873
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1874
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1875
+ }
1876
+
1877
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1878
+
1879
+ const getTicket = {
1880
+ size: 10000,
1881
+ query: {
1882
+ bool: {
1883
+ must: [
1884
+ {
1885
+ term: {
1886
+ 'type.keyword': 'store',
1887
+ },
1888
+ },
1889
+ {
1890
+ term: {
1891
+ 'storeId.keyword': inputData.storeId,
1892
+ },
1893
+ },
1894
+ {
1895
+ term: {
1896
+ 'dateString': inputData.dateString,
1897
+ },
1898
+ },
1899
+ ],
1900
+ },
1901
+ },
1902
+ };
1903
+
1904
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1905
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1906
+ if ( !ticketData || ticketData?.length == 0 ) {
1907
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1908
+ }
1909
+
1910
+ const record = {
1911
+
1912
+ status: 'Approver-Closed',
1913
+ revicedFootfall: revisedFootfall,
1914
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1915
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1916
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1917
+ // createdByEmail: req?.user?.email,
1918
+ // createdByUserName: req?.user?.userName,
1919
+ // createdByRole: req?.user?.role,
1920
+
1921
+ };
1922
+
1923
+
1924
+ // Retrieve client footfallDirectoryConfigs revision
1925
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
1926
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1927
+
1928
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1929
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1930
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1931
+ const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
1932
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
1933
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1934
+ logger.info( { revised, tangoReview } );
1935
+ if (
1936
+ isAutoCloseEnable === true &&
1937
+ revisedPercentage >= autoCloseAccuracyValue
1938
+ ) {
1939
+ logger.info( { revisedPercentage, autoCloseAccuracyValue, isAutoCloseEnable } );
1940
+ record.status = 'Approver-Closed';
1941
+ // Only keep or modify mappingInfo items with type "review"
1942
+ if ( Array.isArray( record.mappingInfo ) ) {
1943
+ const temp = record.mappingInfo
1944
+ .filter( ( item ) => item.type === 'approve' )
1945
+ .map( ( item ) => ( {
1946
+ ...item,
1947
+
1948
+ mode: inputData.mode,
1949
+ revicedFootfall: revisedFootfall,
1950
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1951
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1952
+ count: tempAcc,
1953
+ revisedDetail: formattedTaggingData,
1954
+ status: 'Closed',
1955
+ createdByEmail: req?.user?.email,
1956
+ createdByUserName: req?.user?.userName,
1957
+ createdByRole: req?.user?.role,
1958
+ createdAt: new Date(),
1959
+ } ) );
1960
+
1961
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1962
+ ...temp ];
1963
+ // If updating the mapping config to mark [i].status as 'Closed'
1964
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
1965
+ if ( Array.isArray( record.mappingInfo ) ) {
1966
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1967
+ return {
1968
+ ...item,
1969
+ status: 'Closed',
1970
+ };
1971
+ } );
1972
+ }
1973
+ // If no review mapping existed, push a new one
1974
+ if ( record.mappingInfo.length === 0 ) {
1975
+ record.mappingInfo.push( {
1976
+ type: 'approve',
1977
+ mode: inputData.mode,
1978
+ revicedFootfall: revisedFootfall,
1979
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1980
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1981
+ count: tempAcc,
1982
+ revisedDetail: formattedTaggingData,
1983
+ status: 'Closed',
1984
+ createdByEmail: req?.user?.email,
1985
+ createdByUserName: req?.user?.userName,
1986
+ createdByRole: req?.user?.role,
1987
+ createdAt: new Date(),
1988
+ } );
1989
+ }
1990
+ }
1991
+ record.mappingInfo.push(
1992
+ {
1993
+ type: 'finalRevision',
1994
+ mode: inputData.mode,
1995
+ revicedFootfall: revisedFootfall,
1996
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1997
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
1998
+ count: tempAcc,
1999
+ revisedDetail: formattedTaggingData,
2000
+ status: 'Closed',
2001
+ createdByEmail: req?.user?.email,
2002
+ createdByUserName: req?.user?.userName,
2003
+ createdByRole: req?.user?.role,
2004
+ createdAt: new Date(),
2005
+ },
2006
+ );
2007
+ } else if ( revised < tangoReview ) {
2008
+ logger.info( { revised, tangoReview } );
2009
+ // If ticket is closed, do not proceed with revision mapping
2010
+
2011
+ // Default fallbacks
2012
+
2013
+ let approverMapping = null;
2014
+ let tangoReviewMapping = null;
2015
+
2016
+ record.status = 'Approver-Closed';
2017
+ // Only keep or modify mappingInfo items with type "review"
2018
+ if ( Array.isArray( record.mappingInfo ) ) {
2019
+ const temp = record.mappingInfo
2020
+ .filter( ( item ) => item.type === 'approve' )
2021
+ .map( ( item ) => ( {
2022
+ ...item,
2023
+ mode: inputData.mode,
2024
+ revicedFootfall: revisedFootfall,
2025
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2026
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
2027
+ count: tempAcc,
2028
+ revisedDetail: formattedTaggingData,
2029
+ status: 'Under Tango Review',
2030
+ createdByEmail: req?.user?.email,
2031
+ createdByUserName: req?.user?.userName,
2032
+ createdByRole: req?.user?.role,
2033
+ createdAt: new Date(),
2034
+ } ) );
2035
+
2036
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
2037
+ ...temp ];
2038
+
2039
+ // If no review mapping existed, push a new one
2040
+ if ( record.mappingInfo.length === 0 ) {
2041
+ record.mappingInfo.push( {
2042
+ type: 'approve',
2043
+ mode: inputData.mode,
2044
+ revicedFootfall: revisedFootfall,
2045
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2046
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
2047
+ count: tempAcc,
2048
+ revisedDetail: formattedTaggingData,
2049
+ status: 'Under Tango Review',
2050
+ createdByEmail: req?.user?.email,
2051
+ createdByUserName: req?.user?.userName,
2052
+ createdByRole: req?.user?.role,
2053
+ createdAt: new Date(),
2054
+ } );
2055
+ }
2056
+ }
2057
+
2058
+ // Find out which roles have isChecked true
2059
+
2060
+ // for ( const r of revisionArray ) {
2061
+ // if ( r.actionType === 'tango' && r.isChecked === true ) {
2062
+ tangoReviewMapping = {
2063
+ type: 'tangoreview',
2064
+ // revicedFootfall: revisedFootfall,
2065
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2066
+ count: tempAcc,
2067
+ revisedDetail: formattedTaggingData,
2068
+ status: 'Open',
2069
+ dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
2070
+ };
2071
+ // }
2072
+ // }
2073
+
2074
+
2075
+ if ( approverMapping ) {
2076
+ // If approver and checked
2077
+ record.mappingInfo.push( approverMapping );
2078
+ } else if ( tangoReviewMapping ) {
2079
+ // If none above, then tangoReview
2080
+ record.mappingInfo.push( tangoReviewMapping );
2081
+ }
2082
+ } else {
2083
+ logger.info( { msg: '...............1', revised, tangoReview } );
2084
+ if ( Array.isArray( record.mappingInfo ) ) {
2085
+ const temp = record.mappingInfo
2086
+ .filter( ( item ) => item.type === 'approve' )
2087
+ .map( ( item ) => ( {
2088
+ ...item,
2089
+ mode: inputData.mode,
2090
+ revicedFootfall: revisedFootfall,
2091
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2092
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
2093
+ count: tempAcc,
2094
+ revisedDetail: formattedTaggingData,
2095
+ status: 'Closed',
2096
+ createdByEmail: req?.user?.email,
2097
+ createdByUserName: req?.user?.userName,
2098
+ createdByRole: req?.user?.role,
2099
+ createdAt: new Date(),
2100
+ } ) );
2101
+
2102
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
2103
+ ...temp ];
2104
+ if ( Array.isArray( record.mappingInfo ) ) {
2105
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
2106
+ return {
2107
+ ...item,
2108
+ status: 'Closed',
2109
+ };
2110
+ } );
2111
+ }
2112
+ // If no review mapping existed, push a new one
2113
+ if ( record.mappingInfo.length === 0 ) {
2114
+ record.mappingInfo.push( {
2115
+ type: 'approve',
2116
+ mode: inputData.mode,
2117
+ revicedFootfall: revisedFootfall,
2118
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2119
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
2120
+ count: tempAcc,
2121
+ revisedDetail: formattedTaggingData,
2122
+ status: 'Closed',
2123
+ createdByEmail: req?.user?.email,
2124
+ createdByUserName: req?.user?.userName,
2125
+ createdByRole: req?.user?.role,
2126
+ createdAt: new Date(),
2127
+ } );
2128
+ }
2129
+ }
2130
+ record.mappingInfo.push(
2131
+ {
2132
+ type: 'finalRevision',
2133
+ mode: inputData.mode,
2134
+ revicedFootfall: revisedFootfall,
2135
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
2136
+ reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
2137
+ count: tempAcc,
2138
+ revisedDetail: formattedTaggingData,
2139
+ status: 'Closed',
2140
+ createdByEmail: req?.user?.email,
2141
+ createdByUserName: req?.user?.userName,
2142
+ createdByRole: req?.user?.role,
2143
+ createdAt: new Date(),
2144
+ },
2145
+ );
2146
+ }
2147
+ let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
2148
+
2149
+
2150
+ if ( checkapprove.length > 0 ) {
2151
+ let userQuery = [
2152
+ {
2153
+ $match: {
2154
+ clientId: getstoreName.clientId,
2155
+ role: 'admin',
2156
+ isActive: true,
2157
+ },
2158
+ },
2159
+ ];
2160
+ let finduserList = await aggregateUser( userQuery );
2161
+
2162
+
2163
+ for ( let userData of finduserList ) {
2164
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
2165
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
2166
+ let description = `Created on ${createdOn}`;
2167
+
2168
+ let Data = {
2169
+ 'title': title,
2170
+ 'body': description,
2171
+ 'type': 'approve',
2172
+ 'date': record.dateString,
2173
+ 'storeId': record.storeId,
2174
+ 'clientId': record.clientId,
2175
+ 'ticketId': record.ticketId,
2176
+ };
2177
+
2178
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
2179
+
2180
+ if ( ticketsFeature ) {
2181
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
2182
+ if ( userData && userData.fcmToken && notifyuser ) {
2183
+ const fcmToken = userData.fcmToken;
2184
+ await sendPushNotification( title, description, fcmToken, Data );
2185
+ }
2186
+ }
2187
+ }
2188
+ }
2189
+
2190
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
2191
+ console.log( '🚀 ~ ticketReview ~ id:', id );
2192
+ console.log( '🚀 ~ ticketReview ~ id:', id );
2193
+ console.log( '🚀 ~ ticketReview ~ id:', id );
2194
+ if ( record.status==='Approver-Closed' ) {
2195
+ let Ticket = await getOpenSearchById( openSearch.footfallDirectory, id );
2196
+ console.log( '🚀 ~ ticketApprove ~ Ticket:', Ticket?.body );
2197
+ if ( Ticket?.body?._source?.type==='store' ) {
2198
+ let findTagging = Ticket?.body?._source?.mappingInfo.filter( ( data ) => data.type==='tagging' );
2199
+ console.log( '🚀 ~ ticketApprove ~ findTagging[0].createdByEmail:', findTagging[0].createdByEmail );
2200
+ if ( findTagging?.length>0&&findTagging[0].createdByEmail!='' ) {
2201
+ let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
2202
+ let title = `Received response for the Footfall ticket raised.`;
2203
+ let createdOn = dayjs( Ticket?.body?._source?.dateString ).format( 'DD MMM YYYY' );
2204
+ let description = `Raised on ${createdOn}`;
2205
+
2206
+ let Data = {
2207
+ 'title': title,
2208
+ 'body': description,
2209
+ 'type': 'closed',
2210
+ 'date': Ticket?.body?._source?.dateString,
2211
+ 'storeId': Ticket?.body?._source?.storeId,
2212
+ 'clientId': Ticket?.body?._source?.clientId,
2213
+ 'ticketId': Ticket?.body?._source?.ticketId,
2214
+ };
2215
+ if ( userData && userData.fcmToken ) {
2216
+ const fcmToken = userData.fcmToken;
2217
+ await sendPushNotification( title, description, fcmToken, Data );
2218
+ }
2219
+ }
2220
+ }
2221
+ }
2222
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
2223
+
2224
+
2225
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
2226
+ if ( record.status = 'Closed' ) {
2227
+ const query = {
2228
+ storeId: inputData?.storeId,
2229
+ isVideoStream: true,
2230
+ };
2231
+ const getStoreType = await countDocumnetsCamera( query );
2232
+ const revopInfoQuery = {
2233
+ size: 10000,
2234
+ query: {
2235
+ bool: {
2236
+ must: [
2237
+ {
2238
+ term: {
2239
+ 'storeId.keyword': inputData.storeId,
2240
+ },
2241
+ },
2242
+ {
2243
+ term: {
2244
+ 'dateString': inputData.dateString,
2245
+ },
2246
+ },
2247
+ {
2248
+ term: {
2249
+ 'isParent': false,
2250
+ },
2251
+ },
2252
+ {
2253
+ term: {
2254
+ isChecked: true,
2255
+ },
2256
+ },
2257
+ ],
2258
+ },
2259
+ },
2260
+ _source: [ 'tempId' ],
2261
+
2262
+ };
2263
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
2264
+ // Get all tempIds from revopInfo response
2265
+ const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
2266
+ // Prepare management eyeZone query based on storeId and dateString
2267
+ const managerEyeZoneQuery = {
2268
+ size: 1,
2269
+ query: {
2270
+ bool: {
2271
+ must: [
2272
+ {
2273
+ term: {
2274
+ 'storeId.keyword': inputData.storeId,
2275
+ },
2276
+ },
2277
+ {
2278
+ term: {
2279
+ 'storeDate': inputData.dateString,
2280
+ },
2281
+ },
2282
+ ],
2283
+ },
2284
+ },
2285
+ _source: [ 'originalToTrackerCustomerMapping' ],
2286
+ };
2287
+
2288
+ // Query the managerEyeZone index for the matching document
2289
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
2290
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
2291
+ // Extract originalToTrackerCustomerMapping if it exists
2292
+ const mapping =
2293
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
2294
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
2295
+ {};
2296
+
2297
+ // Find tempIds that exist in both revopInfo results and manager mapping
2298
+ const temp = [];
2299
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
2300
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
2301
+ if ( isSendMessge == true ) {
2302
+ logger.info( '....1' );
2303
+ }
2304
+ }
2305
+ return res.sendSuccess( 'Ticket closed successfully' );
2306
+ } else {
2307
+ return res.sendError( 'Internal Server Error', 500 );
2308
+ }
2309
+ } catch ( error ) {
2310
+ const err = error.message || 'Internal Server Error';
2311
+ logger.error( { error: err, funtion: 'ticketCreation' } );
2312
+ return res.sendError( err, 500 );
2313
+ }
2314
+ }
2315
+
2316
+ export async function getAssinedStore( user, storeId ) {
2317
+ if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
2318
+ return;
2319
+ }
2320
+
2321
+ const clientId = user.clientId;
2322
+ const storeIds = new Set(
2323
+ user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
2324
+ );
2325
+
2326
+ const addClusterStores = ( clusters ) => {
2327
+ if ( !clusters?.length ) return;
2328
+ for ( const cluster of clusters ) {
2329
+ cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
2330
+ }
2331
+ };
2332
+
2333
+ // Fetch all top-level data in parallel
2334
+ const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
2335
+ findcluster( {
2336
+ clientId,
2337
+ Teamlead: { $elemMatch: { email: user.email } },
2338
+ } ),
2339
+ findteams( {
2340
+ clientId,
2341
+ Teamlead: { $elemMatch: { email: user.email } },
2342
+ } ),
2343
+ findteams( {
2344
+ clientId,
2345
+ users: { $elemMatch: { email: user.email } },
2346
+ } ),
2347
+ ] );
2348
+
2349
+ // 1) Clusters where this user is Teamlead
2350
+ addClusterStores( clustersList );
2351
+
2352
+ // 2) Teams where this user is Teamlead → their users + their clusters
2353
+ if ( teamsList?.length ) {
2354
+ for ( const team of teamsList ) {
2355
+ if ( !team.users?.length ) continue;
2356
+
2357
+ await Promise.all(
2358
+ team.users.map( async ( teamUser ) => {
2359
+ const foundUser = await findOneUser( { _id: teamUser.userId } );
2360
+ if ( !foundUser ) return;
2361
+
2362
+ // Direct assigned stores of that user
2363
+ if ( foundUser.assignedStores?.length ) {
2364
+ foundUser.assignedStores.forEach( ( store ) =>
2365
+ storeIds.add( store.storeId ),
2366
+ );
2367
+ }
2368
+
2369
+ // Clusters where this user is Teamlead
2370
+ const userClustersList = await findcluster( {
2371
+ clientId,
2372
+ Teamlead: { $elemMatch: { email: foundUser.email } },
2373
+ } );
2374
+ addClusterStores( userClustersList );
2375
+ } ),
2376
+ );
2377
+ }
2378
+ }
2379
+
2380
+ // 3) Teams where this user is a member → clusters by teamName
2381
+ if ( teamMemberList?.length ) {
2382
+ for ( const team of teamMemberList ) {
2383
+ const clusterList = await findcluster( {
2384
+ clientId,
2385
+ teams: { $elemMatch: { name: team.teamName } },
2386
+ } );
2387
+ addClusterStores( clusterList );
2388
+ }
2389
+ }
2390
+
2391
+ const assignedStores = Array.from( storeIds );
2392
+
2393
+ // Previously you returned `true` in both branches.
2394
+ // Assuming you actually want to check membership:
2395
+ return assignedStores.includes( storeId );
2396
+ }
684
2397