tango-app-api-infra 3.9.5-vms.6 → 3.9.5-vms.61

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,26 +233,26 @@ 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 ) ) ) );
231
- logger.info( { ticketsFeature } );
243
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && ( m.isAdd == true || m.isEdit == true ) ) ) );
232
244
  if ( !ticketsFeature ) {
233
245
  return res.sendError( 'Forbidden to Create Ticket', 403 );
234
246
  }
235
247
 
236
248
  // get store info by the storeId into mongo db
237
249
  const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
238
- logger.info( { getstoreName } );
250
+
239
251
  if ( !getstoreName || getstoreName == null ) {
240
252
  return res.sendError( 'The store ID is either inActive or not found', 400 );
241
253
  }
242
254
 
243
255
  // get the footfall count from opensearch
244
- const openSearch = JSON.parse( process.env.OPENSEARCH );
245
256
  const dateString = `${inputData.storeId}_${inputData.dateString}`;
246
257
  const getQuery = {
247
258
  query: {
@@ -261,23 +272,21 @@ export async function ticketCreation( req, res, next ) {
261
272
 
262
273
  const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
263
274
  const hits = getFootfallCount?.body?.hits?.hits || [];
264
- logger.info( { hits } );
265
275
  if ( hits?.[0]?._source?.footfall_count <= 0 ) {
266
276
  return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
267
277
  }
268
278
 
269
279
  // get category details from the client level configuration
270
- const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
271
- logger.info( { getConfig, ta123: getConfig?.footfallDirectoryConfigs } );
280
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1, clientId: 1 } );
272
281
  if ( !getConfig || getConfig == null ) {
273
282
  return res.sendError( 'The Client ID is either not configured or not found', 400 );
274
283
  }
275
284
 
276
285
  // Get taggingLimitation from config (check both possible paths)
277
286
  const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
278
- logger.info( { taggingLimitation, tagginngs: getConfig?.footfallDirectoryConfigs } );
279
287
  // Initialize count object from taggingLimitation
280
- const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
288
+ const tempAcc = [];
289
+ taggingLimitation?.reduce( ( acc, item ) => {
281
290
  if ( item?.type ) {
282
291
  // Convert type to camelCase with "Count" suffix
283
292
  // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
@@ -289,11 +298,21 @@ export async function ticketCreation( req, res, next ) {
289
298
  // Convert first letter to lowercase and append "Count"
290
299
  key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
291
300
  }
292
- acc[key] = 0;
301
+
302
+
303
+ // To change from an object to the desired array structure, assemble an array of objects:
304
+ tempAcc.push( {
305
+ name: item.name,
306
+ value: 0,
307
+ key: key,
308
+ type: item.type,
309
+ } );
310
+
311
+
312
+ return acc;
293
313
  }
294
- return acc;
295
314
  }, {} ) || {};
296
- logger.info( { getCategory } );
315
+
297
316
  // Query OpenSearch revop index to get actual counts for each type
298
317
  if ( taggingLimitation && taggingLimitation.length > 0 ) {
299
318
  const revopQuery = {
@@ -311,6 +330,16 @@ export async function ticketCreation( req, res, next ) {
311
330
  'dateString': inputData.dateString,
312
331
  },
313
332
  },
333
+ {
334
+ term: {
335
+ 'isParent': false,
336
+ },
337
+ },
338
+ {
339
+ term: {
340
+ isChecked: true,
341
+ },
342
+ },
314
343
  ],
315
344
  },
316
345
  },
@@ -324,51 +353,38 @@ export async function ticketCreation( req, res, next ) {
324
353
  },
325
354
  };
326
355
 
327
- try {
328
- const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
329
- logger.info( { revopData: revopData } );
330
- const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
331
-
332
- // Map OpenSearch revopsType values to count object keys
333
- buckets.forEach( ( bucket ) => {
334
- const revopsType = bucket.key?.toLowerCase();
335
- const count = bucket.doc_count || 0;
336
-
337
- // Map the revopsType to the correct count key
338
- // Handle variations: duplicate, duplicateImages, duplicateimages
339
- if ( revopsType === 'duplicate' ) {
340
- if ( getCategory.hasOwnProperty( 'duplicateCount' ) ) {
341
- getCategory.duplicateCount = count;
342
- }
343
- } else if ( revopsType === 'employee' ) {
344
- if ( getCategory.hasOwnProperty( 'employeeCount' ) ) {
345
- getCategory.employeeCount = count;
346
- }
347
- } else if ( revopsType === 'housekeeping' ) {
348
- if ( getCategory.hasOwnProperty( 'houseKeepingCount' ) ) {
349
- getCategory.houseKeepingCount = count;
350
- }
351
- } else if ( revopsType === 'junk' ) {
352
- if ( getCategory.hasOwnProperty( 'junkCount' ) ) {
353
- getCategory.junkCount = count;
354
- }
356
+
357
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
358
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
359
+
360
+ // Map OpenSearch revopsType values to count object keys
361
+ buckets.forEach( ( bucket ) => {
362
+ const revopsType = bucket.key;
363
+ const count = bucket.doc_count || 0;
364
+
365
+
366
+ if ( Array.isArray( tempAcc ) ) {
367
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
368
+ const accMatch = tempAcc.find(
369
+ ( acc ) =>
370
+ acc.type &&
371
+ acc.type === revopsType,
372
+ );
373
+
374
+ if ( accMatch && accMatch.key ) {
375
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
355
376
  }
356
- } );
357
- } catch ( error ) {
358
- logger.error( { error: error, message: 'Error fetching revop counts', function: 'footfallDirectoryTicket-ticketCreation' } );
359
- // Continue with default 0 values if query fails
360
- }
377
+ }
378
+ } );
361
379
  }
362
380
 
363
- logger.info( { getCategory: getCategory } );
364
381
 
365
382
  // Calculate revisedFootfall: footfallCount - (sum of all counts)
366
- const totalCount = ( getCategory.duplicateCount || 0 ) +
367
- ( getCategory.employeeCount || 0 ) +
368
- ( getCategory.houseKeepingCount || 0 ) +
369
- ( getCategory.junkCount || 0 );
383
+
384
+ const totalCount = Array.isArray( tempAcc ) ?
385
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
386
+ 0;
370
387
  const footfallCount = hits?.[0]?._source?.footfall_count || 0;
371
- logger.info( { footfallCount, totalCount } );
372
388
  const revisedFootfall = Math.max( 0, footfallCount - totalCount );
373
389
  if ( footfallCount - revisedFootfall == 0 ) {
374
390
  return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
@@ -400,91 +416,92 @@ export async function ticketCreation( req, res, next ) {
400
416
  return res.sendError( 'You don’t have any tagged images right now', 400 );
401
417
  }
402
418
  const formattedTaggingData = formatRevopTaggingHits( taggingImages );
403
- logger.info( { revopTaggingData: formattedTaggingData } );
404
419
 
405
420
  const record = {
406
421
  storeId: inputData.storeId,
422
+ type: 'store',
407
423
  dateString: inputData.dateString,
408
424
  storeName: getstoreName?.storeName,
409
- ticketName: inputData.ticketName|| 'footfall-directory',
425
+ ticketName: inputData.ticketName || 'footfall-directory',
410
426
  footfallCount: footfallCount,
411
427
  clientId: getstoreName?.clientId,
412
428
  ticketId: 'TE_FDT_' + new Date().valueOf(),
413
429
  createdAt: new Date(),
414
430
  updatedAt: new Date(),
415
- status: 'raised',
431
+ status: 'Raised',
432
+ comments: inputData?.comments || '',
416
433
  revicedFootfall: revisedFootfall,
417
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
434
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
418
435
  mappingInfo: [
419
436
  {
420
437
  type: 'tagging',
421
438
  mode: inputData.mode,
422
439
  revicedFootfall: revisedFootfall,
423
- count: getCategory,
440
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
441
+ count: tempAcc,
424
442
  revisedDetail: formattedTaggingData,
425
- status: 'raised',
443
+ status: 'Raised',
426
444
  createdByEmail: req?.user?.email,
427
445
  createdByUserName: req?.user?.userName,
428
446
  createdByRole: req?.user?.role,
447
+ createdAt: new Date(),
429
448
  },
430
449
  ],
431
450
  };
432
451
 
433
452
 
434
453
  // Retrieve client footfallDirectoryConfigs revision
435
- let isAutoCloseEnable = false;
436
- let autoCloseAccuracy = '95%';
437
- try {
438
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
439
- if ( clientData?.footfallDirectoryConfigs ) {
440
- isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
441
- autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
442
- }
443
- } catch ( e ) {
444
- isAutoCloseEnable = false;
445
- autoCloseAccuracy = '95%';
446
- }
454
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
455
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
447
456
 
448
- let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || '95' ).replace( '%', '' ) );
449
- let revisedPercentage = 0;
450
- if ( typeof getCategory === 'number' && getCategory > 0 ) {
451
- revisedPercentage = ( revisedFootfall / getCategory ) * 100;
452
- }
457
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
458
+
459
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
460
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
453
461
 
454
462
  // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
455
463
  if (
456
464
  isAutoCloseEnable === true &&
457
465
  revisedPercentage >= autoCloseAccuracyValue
458
466
  ) {
459
- record.status = 'closed';
467
+ record.status = 'Closed';
460
468
  record.mappingInfo = [
461
469
  {
462
470
  type: 'tagging',
463
471
  mode: inputData.mode,
464
472
  revicedFootfall: revisedFootfall,
465
- count: getCategory,
473
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
474
+ count: tempAcc,
475
+ revisedDetail: formattedTaggingData,
476
+ status: 'Closed',
477
+ createdByEmail: req?.user?.email,
478
+ createdByUserName: req?.user?.userName,
479
+ createdByRole: req?.user?.role,
480
+ createdAt: new Date(),
481
+ },
482
+ {
483
+ type: 'finalRevision',
484
+ mode: inputData.mode,
485
+ revicedFootfall: revisedFootfall,
486
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
487
+ count: tempAcc,
466
488
  revisedDetail: formattedTaggingData,
467
- status: 'closed',
489
+ status: 'Closed',
468
490
  createdByEmail: req?.user?.email,
469
491
  createdByUserName: req?.user?.userName,
470
492
  createdByRole: req?.user?.role,
493
+ createdAt: new Date(),
471
494
  },
472
495
  ];
473
496
  } else {
474
- // If ticket is closed, do not proceed with revision mapping
497
+ // If ticket is closed, do not proceed with revision mapping
475
498
  let revisionArray = [];
476
- try {
477
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
478
- revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
479
- } catch ( e ) {
480
- revisionArray = [];
481
- }
482
499
 
500
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
483
501
  // Default fallbacks
484
502
  let revisionMapping = null;
485
503
  let approverMapping = null;
486
504
  let tangoReviewMapping = null;
487
-
488
505
  // Find out which roles have isChecked true
489
506
  if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
490
507
  for ( const r of revisionArray ) {
@@ -492,25 +509,28 @@ export async function ticketCreation( req, res, next ) {
492
509
  revisionMapping = {
493
510
  type: 'review',
494
511
  revicedFootfall: revisedFootfall,
495
- count: getCategory,
512
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
513
+ count: tempAcc,
496
514
  revisedDetail: formattedTaggingData,
497
- status: 'open',
515
+ status: 'Open',
498
516
  };
499
517
  } else if ( r.actionType === 'approver' && r.isChecked === true ) {
500
518
  approverMapping = {
501
- type: 'approver',
519
+ type: 'approve',
502
520
  revicedFootfall: revisedFootfall,
503
- count: getCategory,
521
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
522
+ count: tempAcc,
504
523
  revisedDetail: formattedTaggingData,
505
- status: 'open',
524
+ status: 'Open',
506
525
  };
507
526
  } else if ( r.actionType === 'tango' && r.isChecked === true ) {
508
527
  tangoReviewMapping = {
509
- type: 'tango-review',
528
+ type: 'tangoreview',
510
529
  revicedFootfall: revisedFootfall,
511
- count: getCategory,
530
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
531
+ count: tempAcc,
512
532
  revisedDetail: formattedTaggingData,
513
- status: 'open',
533
+ status: 'Open',
514
534
  };
515
535
  }
516
536
  }
@@ -518,36 +538,161 @@ export async function ticketCreation( req, res, next ) {
518
538
 
519
539
  // Insert appropriate mappingInfo blocks
520
540
  if ( revisionMapping ) {
521
- // If reviewer and checked
541
+ // If reviewer and checked
522
542
  record.mappingInfo.push( revisionMapping );
523
543
  } else if ( approverMapping ) {
524
- // If approver and checked
544
+ // If approver and checked
525
545
  record.mappingInfo.push( approverMapping );
526
546
  } else if ( tangoReviewMapping ) {
527
- // If none above, then tangoReview
547
+ // If none above, then tangoReview
528
548
  record.mappingInfo.push( tangoReviewMapping );
529
549
  }
530
550
  }
531
551
 
552
+ let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
553
+ let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
554
+
555
+ if ( checkreview.length > 0 || checkapprove.length > 0 ) {
556
+ let userQuery = [
557
+ {
558
+ $match: {
559
+ clientId: getstoreName.clientId,
560
+ role: 'admin',
561
+ },
562
+ },
563
+ ];
564
+ let finduserList = await aggregateUser( userQuery );
565
+
566
+
567
+ for ( let userData of finduserList ) {
568
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
569
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
570
+ let description = `Created on ${createdOn}`;
571
+ let Data = {
572
+ 'title': title,
573
+ 'body': description,
574
+ 'type': 'create',
575
+ 'date': record.dateString,
576
+ 'storeId': record.storeId,
577
+ 'clientId': record.clientId,
578
+ 'ticketId': record.ticketId,
579
+ };
580
+
581
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
582
+
583
+ if ( ticketsFeature ) {
584
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
585
+ if ( userData && userData.fcmToken && notifyuser ) {
586
+ const fcmToken = userData.fcmToken;
587
+ await sendPushNotification( title, description, fcmToken, Data );
588
+ }
589
+ }
590
+ }
591
+ }
592
+
532
593
 
533
594
  const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
534
595
  const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
535
596
  if ( insertResult && insertResult.statusCode === 201 ) {
536
- // 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
597
+ // After successful ticket creation, update status to "submitted" in revop index for the relevant records
598
+
599
+
600
+ const bulkUpdateBody = taggingImages.map( ( img ) => [
601
+ { update: { _index: openSearch.revop, _id: img._id } },
602
+ { doc: { status: 'submitted' } },
603
+ ] ).flat();
604
+
605
+ if ( bulkUpdateBody.length > 0 ) {
606
+ await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
549
607
  }
550
608
 
609
+ if ( record.status = 'Closed' ) {
610
+ const query = {
611
+ storeId: inputData?.storeId,
612
+ isVideoStream: true,
613
+ };
614
+ const getStoreType = await countDocumnetsCamera( query );
615
+ const revopInfoQuery = {
616
+ size: 10000,
617
+ query: {
618
+ bool: {
619
+ must: [
620
+ {
621
+ term: {
622
+ 'storeId.keyword': inputData.storeId,
623
+ },
624
+ },
625
+ {
626
+ term: {
627
+ 'dateString': inputData.dateString,
628
+ },
629
+ },
630
+ {
631
+ term: {
632
+ 'isParent': false,
633
+ },
634
+ },
635
+ {
636
+ term: {
637
+ isChecked: true,
638
+ },
639
+ },
640
+ ],
641
+ },
642
+ },
643
+ _source: [ 'tempId' ],
644
+
645
+ };
646
+
647
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
648
+
649
+ // Get all tempIds from revopInfo response
650
+ const tempIds =
651
+ revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
652
+ // Prepare management eyeZone query based on storeId and dateString
653
+ const managerEyeZoneQuery = {
654
+ size: 1,
655
+ query: {
656
+ bool: {
657
+ must: [
658
+ {
659
+ term: {
660
+ 'storeId.keyword': inputData.storeId,
661
+ },
662
+ },
663
+ {
664
+ term: {
665
+ 'storeDate': inputData.dateString,
666
+ },
667
+ },
668
+ ],
669
+ },
670
+ },
671
+ _source: [ 'originalToTrackerCustomerMapping' ],
672
+ };
673
+
674
+ // Query the managerEyeZone index for the matching document
675
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
676
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
677
+ // Extract originalToTrackerCustomerMapping if it exists
678
+ const mapping =
679
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
680
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
681
+ {};
682
+
683
+ // If you want to compare or find matching tempIds in the mapping
684
+ // The mapping is { "1": tempId1, ... }, so get values as array of tempIds
685
+ // const managerMappedTempIds = Object.values( mapping );
686
+
687
+ // Find tempIds that exist in both revopInfo results and manager mapping
688
+ const temp = [];
689
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
690
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
691
+ if ( isSendMessge == true ) {
692
+ logger.info( '....1' );
693
+ // return true; // res.sendSuccess( 'Ticket has been updated successfully' );
694
+ } // Example: log or use these tempIds for further logic
695
+ }
551
696
  // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
552
697
 
553
698
  if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
@@ -557,7 +702,7 @@ export async function ticketCreation( req, res, next ) {
557
702
  // req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
558
703
  const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
559
704
  const storeId = inputData.storeId;
560
- const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
705
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
561
706
 
562
707
 
563
708
  const formatDate = ( d ) =>
@@ -573,14 +718,14 @@ export async function ticketCreation( req, res, next ) {
573
718
  const startDateObj = new Date( currentDateObj );
574
719
 
575
720
  if ( breachDays === 30 ) {
576
- // Consider within this month
721
+ // Consider within this month
577
722
  startDateObj.setDate( 1 ); // First day of current month
578
723
  } else if ( breachDays === 60 ) {
579
- // Consider this month and last month
724
+ // Consider this month and last month
580
725
  startDateObj.setMonth( startDateObj.getMonth() - 1 );
581
726
  startDateObj.setDate( 1 ); // First day of last month
582
727
  } else {
583
- // For other values, calculate months from days
728
+ // For other values, calculate months from days
584
729
  const breachMonths = Math.ceil( breachDays / 30 );
585
730
  startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
586
731
  startDateObj.setDate( 1 );
@@ -628,21 +773,21 @@ export async function ticketCreation( req, res, next ) {
628
773
  }
629
774
 
630
775
  if ( breachTicketsCount >= breachCount ) {
631
- // Calculate remaining future days in the config period
776
+ // Calculate remaining future days in the config period
632
777
  const futureDates = [];
633
778
 
634
779
  // Calculate end date of config period
635
780
  const configEndDateObj = new Date( currentDateObj );
636
781
  if ( breachDays === 30 ) {
637
- // End of current month
782
+ // End of current month
638
783
  configEndDateObj.setMonth( configEndDateObj.getMonth() + 1 );
639
784
  configEndDateObj.setDate( 0 ); // Last day of current month
640
785
  } else if ( breachDays === 60 ) {
641
- // End of next month
786
+ // End of next month
642
787
  configEndDateObj.setMonth( configEndDateObj.getMonth() + 2 );
643
788
  configEndDateObj.setDate( 0 ); // Last day of next month
644
789
  } else {
645
- // For other values, add the remaining days
790
+ // For other values, add the remaining days
646
791
  const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
647
792
  configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
648
793
  }
@@ -671,14 +816,1048 @@ export async function ticketCreation( req, res, next ) {
671
816
  }
672
817
  }
673
818
  }
819
+ const sqsName = sqs.vmsPickleExtention;
820
+ const sqsProduceQueue = {
821
+ QueueUrl: `${sqs.url}${sqsName}`,
822
+ MessageBody: JSON.stringify( {
823
+ store_id: inputData?.storeId,
824
+ store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
825
+ primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
826
+ time: new Date(),
827
+ } ),
828
+ MessageGroupId: 'revops-pickle',
829
+ MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
830
+ };
831
+ const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
832
+
833
+ if ( sqsQueue.statusCode ) {
834
+ logger.error( {
835
+ error: `${sqsQueue}`,
836
+ type: 'SQS_NOT_SEND_ERROR',
837
+ } );
838
+ }
839
+
674
840
 
675
841
  return res.sendSuccess( 'Ticket raised successfully' );
676
842
  }
677
843
  } catch ( error ) {
678
844
  const err = error.message || 'Internal Server Error';
679
- logger.error( { error: err, funtion: 'ticketCreation' } );
845
+ logger.error( { error: error, funtion: 'ticketCreation' } );
680
846
  return res.sendError( err, 500 );
681
847
  }
682
848
  }
683
849
 
850
+ export async function ticketReview( req, res, next ) {
851
+ try {
852
+ const inputData = req.body;
853
+ if ( inputData?.type !== 'review' ) {
854
+ return next();
855
+ }
856
+ // check the createtion permission from the user permission
857
+ const userInfo = req?.user;
858
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
859
+ if ( !ticketsFeature ) {
860
+ return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
861
+ }
862
+
863
+ // get store info by the storeId into mongo db
864
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
865
+
866
+ if ( !getstoreName || getstoreName == null ) {
867
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
868
+ }
869
+
870
+ // get the footfall count from opensearch
871
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
872
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
873
+ const getQuery = {
874
+ query: {
875
+ terms: {
876
+ _id: [ dateString ],
877
+ },
878
+ },
879
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
880
+ sort: [
881
+ {
882
+ date_iso: {
883
+ order: 'desc',
884
+ },
885
+ },
886
+ ],
887
+ };
888
+
889
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
890
+ const hits = getFootfallCount?.body?.hits?.hits || [];
891
+ logger.info( { hits } );
892
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
893
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
894
+ }
895
+
896
+ // get category details from the client level configuration
897
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
898
+ if ( !getConfig || getConfig == null ) {
899
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
900
+ }
901
+
902
+ // Get taggingLimitation from config (check both possible paths)
903
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
904
+ // Initialize count object from taggingLimitation
905
+ const tempAcc = [];
906
+ taggingLimitation?.reduce( ( acc, item ) => {
907
+ if ( item?.type ) {
908
+ // Convert type to camelCase with "Count" suffix
909
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
910
+ const typeLower = item.type.toLowerCase();
911
+ let key;
912
+ if ( typeLower === 'housekeeping' ) {
913
+ key = 'houseKeepingCount';
914
+ } else {
915
+ // Convert first letter to lowercase and append "Count"
916
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
917
+ }
918
+
919
+
920
+ // To change from an object to the desired array structure, assemble an array of objects:
921
+ tempAcc.push( {
922
+ name: item.name,
923
+ value: 0,
924
+ key: key,
925
+ type: item.type,
926
+ } );
927
+
928
+
929
+ return acc;
930
+ }
931
+ }, {} ) || {};
932
+
933
+ // Query OpenSearch revop index to get actual counts for each type
934
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
935
+ const revopQuery = {
936
+ size: 0,
937
+ query: {
938
+ bool: {
939
+ must: [
940
+ {
941
+ term: {
942
+ 'storeId.keyword': inputData.storeId,
943
+ },
944
+ },
945
+ {
946
+ term: {
947
+ 'dateString': inputData.dateString,
948
+ },
949
+ },
950
+ {
951
+ term: {
952
+ 'isParent': false,
953
+ },
954
+ },
955
+ {
956
+ term: {
957
+ isChecked: true,
958
+ },
959
+ },
960
+ ],
961
+ },
962
+ },
963
+ aggs: {
964
+ type_counts: {
965
+ terms: {
966
+ field: 'revopsType.keyword',
967
+ size: 100,
968
+ },
969
+ },
970
+ },
971
+ };
972
+
973
+
974
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
975
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
976
+
977
+ // Map OpenSearch revopsType values to count object keys
978
+ buckets.forEach( ( bucket ) => {
979
+ const revopsType = bucket.key;
980
+ const count = bucket.doc_count || 0;
981
+
982
+
983
+ if ( Array.isArray( tempAcc ) ) {
984
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
985
+ const accMatch = tempAcc.find(
986
+ ( acc ) =>
987
+ acc.type &&
988
+ acc.type === revopsType,
989
+ );
990
+
991
+ if ( accMatch && accMatch.key ) {
992
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
993
+ }
994
+ }
995
+ } );
996
+ }
997
+
998
+
999
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1000
+
1001
+ const totalCount = Array.isArray( tempAcc ) ?
1002
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1003
+ 0;
1004
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1005
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1006
+ if ( footfallCount - revisedFootfall == 0 ) {
1007
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1008
+ }
1009
+ const taggingData = {
1010
+ size: 10000,
1011
+ query: {
1012
+ bool: {
1013
+ must: [
1014
+ {
1015
+ term: {
1016
+ 'storeId.keyword': inputData.storeId,
1017
+ },
1018
+ },
1019
+ {
1020
+ term: {
1021
+ 'dateString': inputData.dateString,
1022
+ },
1023
+ },
1024
+ ],
1025
+ },
1026
+ },
1027
+ };
1028
+
1029
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1030
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1031
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1032
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1033
+ }
1034
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
684
1035
 
1036
+ const getTicket = {
1037
+ size: 10000,
1038
+ query: {
1039
+ bool: {
1040
+ must: [
1041
+ {
1042
+ term: {
1043
+ 'storeId.keyword': inputData.storeId,
1044
+ },
1045
+ },
1046
+ {
1047
+ term: {
1048
+ 'dateString': inputData.dateString,
1049
+ },
1050
+ },
1051
+ ],
1052
+ },
1053
+ },
1054
+ };
1055
+
1056
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1057
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1058
+ if ( !ticketData || ticketData?.length == 0 ) {
1059
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1060
+ }
1061
+ const record = {
1062
+
1063
+ status: 'Reviewer-Closed',
1064
+ revicedFootfall: revisedFootfall,
1065
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1066
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1067
+ createdByEmail: req?.user?.email,
1068
+ createdByUserName: req?.user?.userName,
1069
+ createdByRole: req?.user?.role,
1070
+
1071
+ };
1072
+
1073
+ if ( Array.isArray( record.mappingInfo ) ) {
1074
+ const temp = record.mappingInfo
1075
+ .filter( ( item ) => item.type === 'review' )
1076
+ .map( ( item ) => ( {
1077
+ ...item,
1078
+ mode: inputData.mode,
1079
+ revicedFootfall: revisedFootfall,
1080
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1081
+ count: tempAcc,
1082
+ revisedDetail: formattedTaggingData,
1083
+ status: 'Closed',
1084
+ createdByEmail: req?.user?.email,
1085
+ createdByUserName: req?.user?.userName,
1086
+ createdByRole: req?.user?.role,
1087
+ createdAt: new Date(),
1088
+ } ) );
1089
+ record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
1090
+ // If no review mapping existed, push a new one
1091
+ if ( record.mappingInfo.length === 0 ) {
1092
+ record.mappingInfo.push( {
1093
+ type: 'review',
1094
+ mode: inputData.mode,
1095
+ revicedFootfall: revisedFootfall,
1096
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1097
+ count: tempAcc,
1098
+ revisedDetail: formattedTaggingData,
1099
+ status: 'Closed',
1100
+ createdByEmail: req?.user?.email,
1101
+ createdByUserName: req?.user?.userName,
1102
+ createdByRole: req?.user?.role,
1103
+ createdAt: new Date(),
1104
+ } );
1105
+ }
1106
+ }
1107
+
1108
+
1109
+ // Retrieve client footfallDirectoryConfigs revision
1110
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
1111
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1112
+
1113
+
1114
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1115
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1116
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1117
+
1118
+
1119
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1120
+ if (
1121
+ isAutoCloseEnable === true &&
1122
+ revisedPercentage >= autoCloseAccuracyValue
1123
+ ) {
1124
+ record.status = 'Reviewer-Closed';
1125
+ // Only keep or modify mappingInfo items with type "review"
1126
+ if ( Array.isArray( record.mappingInfo ) ) {
1127
+ const temp = record.mappingInfo
1128
+ .filter( ( item ) => item.type === 'review' )
1129
+ .map( ( item ) => ( {
1130
+ ...item,
1131
+ mode: inputData.mode,
1132
+ revicedFootfall: revisedFootfall,
1133
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1134
+ count: tempAcc,
1135
+ revisedDetail: formattedTaggingData,
1136
+ status: 'Closed',
1137
+ createdByEmail: req?.user?.email,
1138
+ createdByUserName: req?.user?.userName,
1139
+ createdByRole: req?.user?.role,
1140
+ } ) );
1141
+
1142
+ const temp2 = record.mappingInfo
1143
+ .filter( ( item ) => item.type === 'tagging' )
1144
+ .map( ( item ) => ( {
1145
+ ...item,
1146
+ mode: inputData.mode,
1147
+ revicedFootfall: revisedFootfall,
1148
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1149
+ count: tempAcc,
1150
+ revisedDetail: formattedTaggingData,
1151
+ status: 'Closed',
1152
+ createdByEmail: req?.user?.email,
1153
+ createdByUserName: req?.user?.userName,
1154
+ createdByRole: req?.user?.role,
1155
+ } ) );
1156
+ record.mappingInfo = [ ...temp2, ...temp ];
1157
+ // If no review mapping existed, push a new one
1158
+ if ( record.mappingInfo.length === 0 ) {
1159
+ record.mappingInfo.push( {
1160
+ type: 'review',
1161
+ mode: inputData.mode,
1162
+ revicedFootfall: revisedFootfall,
1163
+ revicedPerc: 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
+ }
1173
+ record.mappingInfo.push(
1174
+ {
1175
+ type: 'finalRevision',
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
+ createdAt: new Date(),
1186
+ },
1187
+ );
1188
+ } else {
1189
+ // If ticket is closed, do not proceed with revision mapping
1190
+ let revisionArray = [];
1191
+
1192
+
1193
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
1194
+
1195
+
1196
+ // Default fallbacks
1197
+ let revisionMapping = null;
1198
+ let approverMapping = null;
1199
+ let tangoReviewMapping = null;
1200
+
1201
+ // Find out which roles have isChecked true
1202
+ if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
1203
+ for ( const r of revisionArray ) {
1204
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
1205
+ approverMapping = {
1206
+ type: 'approve',
1207
+ revicedFootfall: revisedFootfall,
1208
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1209
+ count: tempAcc,
1210
+ revisedDetail: formattedTaggingData,
1211
+ status: 'Open',
1212
+ };
1213
+ } else if ( r.actionType === 'tango' && r.isChecked === true ) {
1214
+ tangoReviewMapping = {
1215
+ type: 'tangoreview',
1216
+ revicedFootfall: revisedFootfall,
1217
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1218
+ count: tempAcc,
1219
+ revisedDetail: formattedTaggingData,
1220
+ status: 'Open',
1221
+ };
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ // Insert appropriate mappingInfo blocks
1227
+ if ( revisionMapping ) {
1228
+ // If reviewer and checked
1229
+ record.mappingInfo.push( revisionMapping );
1230
+ } else if ( approverMapping ) {
1231
+ // If approver and checked
1232
+ record.mappingInfo.push( approverMapping );
1233
+ } else if ( tangoReviewMapping ) {
1234
+ // If none above, then tangoReview
1235
+ record.mappingInfo.push( tangoReviewMapping );
1236
+ }
1237
+ }
1238
+
1239
+ let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
1240
+
1241
+
1242
+ if ( checkreview.length > 0 ) {
1243
+ let userQuery = [
1244
+ {
1245
+ $match: {
1246
+ clientId: getstoreName.clientId,
1247
+ role: 'admin',
1248
+ },
1249
+ },
1250
+ ];
1251
+ let finduserList = await aggregateUser( userQuery );
1252
+ console.log( '🚀 ~ ticketReview ~ finduserList:', finduserList );
1253
+
1254
+ // return;
1255
+ for ( let userData of finduserList ) {
1256
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1257
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1258
+ let description = `Created on ${createdOn}`;
1259
+
1260
+ let Data = {
1261
+ 'title': title,
1262
+ 'body': description,
1263
+ 'type': 'review',
1264
+ 'date': record.dateString,
1265
+ 'storeId': record.storeId,
1266
+ 'clientId': record.clientId,
1267
+ 'ticketId': record.ticketId,
1268
+ };
1269
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1270
+
1271
+ if ( ticketsFeature ) {
1272
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1273
+ if ( userData && userData.fcmToken && notifyuser ) {
1274
+ const fcmToken = userData.fcmToken;
1275
+ await sendPushNotification( title, description, fcmToken, Data );
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1282
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1283
+
1284
+ if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
1285
+ return res.sendSuccess( 'Ticket closed successfully' );
1286
+ } else {
1287
+ return res.sendError( 'Internal Server Error', 500 );
1288
+ }
1289
+ } catch ( error ) {
1290
+ const err = error.message || 'Internal Server Error';
1291
+ logger.error( { error: err, funtion: 'ticketreview' } );
1292
+ return res.sendError( err, 500 );
1293
+ }
1294
+ }
1295
+
1296
+ export async function ticketApprove( req, res, next ) {
1297
+ try {
1298
+ const inputData = req.body;
1299
+ if ( inputData?.type !== 'approve' ) {
1300
+ return next();
1301
+ }
1302
+ // check the createtion permission from the user permission
1303
+ const userInfo = req?.user;
1304
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1305
+ if ( !ticketsFeature ) {
1306
+ return res.sendError( 'Forbidden to Approve this Ticket', 403 );
1307
+ }
1308
+
1309
+ // get store info by the storeId into mongo db
1310
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
1311
+
1312
+ if ( !getstoreName || getstoreName == null ) {
1313
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
1314
+ }
1315
+
1316
+ // get the footfall count from opensearch
1317
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1318
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
1319
+ const getQuery = {
1320
+ query: {
1321
+ terms: {
1322
+ _id: [ dateString ],
1323
+ },
1324
+ },
1325
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
1326
+ sort: [
1327
+ {
1328
+ date_iso: {
1329
+ order: 'desc',
1330
+ },
1331
+ },
1332
+ ],
1333
+ };
1334
+
1335
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
1336
+ const hits = getFootfallCount?.body?.hits?.hits || [];
1337
+ logger.info( { hits } );
1338
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
1339
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
1340
+ }
1341
+
1342
+ // get category details from the client level configuration
1343
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
1344
+ if ( !getConfig || getConfig == null ) {
1345
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
1346
+ }
1347
+
1348
+ // Get taggingLimitation from config (check both possible paths)
1349
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
1350
+ // Initialize count object from taggingLimitation
1351
+ const tempAcc = [];
1352
+ taggingLimitation?.reduce( ( acc, item ) => {
1353
+ if ( item?.type ) {
1354
+ // Convert type to camelCase with "Count" suffix
1355
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
1356
+ const typeLower = item.type.toLowerCase();
1357
+ let key;
1358
+ if ( typeLower === 'housekeeping' ) {
1359
+ key = 'houseKeepingCount';
1360
+ } else {
1361
+ // Convert first letter to lowercase and append "Count"
1362
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
1363
+ }
1364
+
1365
+
1366
+ // To change from an object to the desired array structure, assemble an array of objects:
1367
+ tempAcc.push( {
1368
+ name: item.name,
1369
+ value: 0,
1370
+ key: key,
1371
+ type: item.type,
1372
+ } );
1373
+
1374
+
1375
+ return acc;
1376
+ }
1377
+ }, {} ) || {};
1378
+
1379
+ // Query OpenSearch revop index to get actual counts for each type
1380
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
1381
+ const revopQuery = {
1382
+ size: 0,
1383
+ query: {
1384
+ bool: {
1385
+ must: [
1386
+ {
1387
+ term: {
1388
+ 'storeId.keyword': inputData.storeId,
1389
+ },
1390
+ },
1391
+ {
1392
+ term: {
1393
+ 'dateString': inputData.dateString,
1394
+ },
1395
+ },
1396
+ ],
1397
+ },
1398
+ },
1399
+ aggs: {
1400
+ type_counts: {
1401
+ terms: {
1402
+ field: 'revopsType.keyword',
1403
+ size: 100,
1404
+ },
1405
+ },
1406
+ },
1407
+ };
1408
+
1409
+
1410
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1411
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1412
+
1413
+ // Map OpenSearch revopsType values to count object keys
1414
+ buckets.forEach( ( bucket ) => {
1415
+ const revopsType = bucket.key;
1416
+ const count = bucket.doc_count || 0;
1417
+
1418
+
1419
+ if ( Array.isArray( tempAcc ) ) {
1420
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1421
+ const accMatch = tempAcc.find(
1422
+ ( acc ) =>
1423
+ acc.type &&
1424
+ acc.type === revopsType,
1425
+ );
1426
+
1427
+ if ( accMatch && accMatch.key ) {
1428
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1429
+ }
1430
+ }
1431
+ } );
1432
+ }
1433
+
1434
+
1435
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1436
+
1437
+ const totalCount = Array.isArray( tempAcc ) ?
1438
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1439
+ 0;
1440
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1441
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1442
+ logger.info( { footfallCount, revisedFootfall } );
1443
+ if ( footfallCount - revisedFootfall == 0 ) {
1444
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1445
+ }
1446
+
1447
+ const taggingData = {
1448
+ size: 10000,
1449
+ query: {
1450
+ bool: {
1451
+ must: [
1452
+ {
1453
+ term: {
1454
+ 'storeId.keyword': inputData.storeId,
1455
+ },
1456
+ },
1457
+ {
1458
+ term: {
1459
+ 'dateString': inputData.dateString,
1460
+ },
1461
+ },
1462
+ {
1463
+ term: {
1464
+ 'isParent': false,
1465
+ },
1466
+ },
1467
+ {
1468
+ term: {
1469
+ isChecked: true,
1470
+ },
1471
+ },
1472
+ ],
1473
+ },
1474
+ },
1475
+ };
1476
+
1477
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1478
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1479
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1480
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1481
+ }
1482
+ logger.info( { taggingImages } );
1483
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1484
+
1485
+ const getTicket = {
1486
+ size: 10000,
1487
+ query: {
1488
+ bool: {
1489
+ must: [
1490
+ {
1491
+ term: {
1492
+ 'storeId.keyword': inputData.storeId,
1493
+ },
1494
+ },
1495
+ {
1496
+ term: {
1497
+ 'dateString': inputData.dateString,
1498
+ },
1499
+ },
1500
+ ],
1501
+ },
1502
+ },
1503
+ };
1504
+
1505
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1506
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1507
+ if ( !ticketData || ticketData?.length == 0 ) {
1508
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1509
+ }
1510
+ logger.info( { ticketData, getFootfallticketData } );
1511
+ const record = {
1512
+
1513
+ status: 'Approver-Closed',
1514
+ revicedFootfall: revisedFootfall,
1515
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1516
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1517
+ createdByEmail: req?.user?.email,
1518
+ createdByUserName: req?.user?.userName,
1519
+ createdByRole: req?.user?.role,
1520
+
1521
+ };
1522
+
1523
+
1524
+ // Retrieve client footfallDirectoryConfigs revision
1525
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
1526
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1527
+
1528
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1529
+ logger.info( { getNumber } );
1530
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1531
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1532
+ const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
1533
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
1534
+ logger.info( { tangoReview, revised } );
1535
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1536
+ if (
1537
+ isAutoCloseEnable === true &&
1538
+ revisedPercentage >= autoCloseAccuracyValue
1539
+ ) {
1540
+ record.status = 'Approver-Closed';
1541
+ // Only keep or modify mappingInfo items with type "review"
1542
+ if ( Array.isArray( record.mappingInfo ) ) {
1543
+ const temp = record.mappingInfo
1544
+ .filter( ( item ) => item.type === 'approve' )
1545
+ .map( ( item ) => ( {
1546
+ ...item,
1547
+
1548
+ mode: inputData.mode,
1549
+ revicedFootfall: revisedFootfall,
1550
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1551
+ count: tempAcc,
1552
+ revisedDetail: formattedTaggingData,
1553
+ status: 'Closed',
1554
+ createdByEmail: req?.user?.email,
1555
+ createdByUserName: req?.user?.userName,
1556
+ createdByRole: req?.user?.role,
1557
+ } ) );
1558
+
1559
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1560
+ ...temp ];
1561
+ // If updating the mapping config to mark [i].status as 'Closed'
1562
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
1563
+ if ( Array.isArray( record.mappingInfo ) ) {
1564
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1565
+ return {
1566
+ ...item,
1567
+ status: 'Closed',
1568
+ };
1569
+ } );
1570
+ }
1571
+ // If no review mapping existed, push a new one
1572
+ if ( record.mappingInfo.length === 0 ) {
1573
+ record.mappingInfo.push( {
1574
+ type: 'approve',
1575
+ mode: inputData.mode,
1576
+ revicedFootfall: revisedFootfall,
1577
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1578
+ count: tempAcc,
1579
+ revisedDetail: formattedTaggingData,
1580
+ status: 'Closed',
1581
+ createdByEmail: req?.user?.email,
1582
+ createdByUserName: req?.user?.userName,
1583
+ createdByRole: req?.user?.role,
1584
+ } );
1585
+ }
1586
+ }
1587
+ record.mappingInfo.push(
1588
+ {
1589
+ type: 'finalRevision',
1590
+ mode: inputData.mode,
1591
+ revicedFootfall: revisedFootfall,
1592
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1593
+ count: tempAcc,
1594
+ revisedDetail: formattedTaggingData,
1595
+ status: 'Closed',
1596
+ createdByEmail: req?.user?.email,
1597
+ createdByUserName: req?.user?.userName,
1598
+ createdByRole: req?.user?.role,
1599
+ createdAt: new Date(),
1600
+ },
1601
+ );
1602
+ } else if ( revised < tangoReview ) {
1603
+ // If ticket is closed, do not proceed with revision mapping
1604
+
1605
+ // Default fallbacks
1606
+
1607
+ let approverMapping = null;
1608
+ let tangoReviewMapping = null;
1609
+
1610
+ record.status = 'Approver-Closed';
1611
+ // Only keep or modify mappingInfo items with type "review"
1612
+ if ( Array.isArray( record.mappingInfo ) ) {
1613
+ const temp = record.mappingInfo
1614
+ .filter( ( item ) => item.type === 'approve' )
1615
+ .map( ( item ) => ( {
1616
+ ...item,
1617
+ mode: inputData.mode,
1618
+ revicedFootfall: revisedFootfall,
1619
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1620
+ count: tempAcc,
1621
+ revisedDetail: formattedTaggingData,
1622
+ status: 'Closed',
1623
+ createdByEmail: req?.user?.email,
1624
+ createdByUserName: req?.user?.userName,
1625
+ createdByRole: req?.user?.role,
1626
+ } ) );
1627
+
1628
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1629
+ ...temp ];
1630
+ if ( Array.isArray( record.mappingInfo ) ) {
1631
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1632
+ return {
1633
+ ...item,
1634
+ status: 'Closed',
1635
+ };
1636
+ } );
1637
+ }
1638
+ // If no review mapping existed, push a new one
1639
+ if ( record.mappingInfo.length === 0 ) {
1640
+ record.mappingInfo.push( {
1641
+ type: 'approve',
1642
+ mode: inputData.mode,
1643
+ revicedFootfall: revisedFootfall,
1644
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1645
+ count: tempAcc,
1646
+ revisedDetail: formattedTaggingData,
1647
+ status: 'Closed',
1648
+ createdByEmail: req?.user?.email,
1649
+ createdByUserName: req?.user?.userName,
1650
+ createdByRole: req?.user?.role,
1651
+ } );
1652
+ }
1653
+ }
1654
+
1655
+ // Find out which roles have isChecked true
1656
+
1657
+ // for ( const r of revisionArray ) {
1658
+ // if ( r.actionType === 'tango' && r.isChecked === true ) {
1659
+ tangoReviewMapping = {
1660
+ type: 'tangoreview',
1661
+ revicedFootfall: revisedFootfall,
1662
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1663
+ count: tempAcc,
1664
+ revisedDetail: formattedTaggingData,
1665
+ status: 'Open',
1666
+ };
1667
+ // }
1668
+ // }
1669
+
1670
+
1671
+ if ( approverMapping ) {
1672
+ // If approver and checked
1673
+ record.mappingInfo.push( approverMapping );
1674
+ } else if ( tangoReviewMapping ) {
1675
+ // If none above, then tangoReview
1676
+ record.mappingInfo.push( tangoReviewMapping );
1677
+ }
1678
+ } else {
1679
+ if ( Array.isArray( record.mappingInfo ) ) {
1680
+ const temp = record.mappingInfo
1681
+ .filter( ( item ) => item.type === 'approve' )
1682
+ .map( ( item ) => ( {
1683
+ ...item,
1684
+ mode: inputData.mode,
1685
+ revicedFootfall: revisedFootfall,
1686
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1687
+ count: tempAcc,
1688
+ revisedDetail: formattedTaggingData,
1689
+ status: 'Closed',
1690
+ createdByEmail: req?.user?.email,
1691
+ createdByUserName: req?.user?.userName,
1692
+ createdByRole: req?.user?.role,
1693
+ } ) );
1694
+
1695
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1696
+ ...temp ];
1697
+ if ( Array.isArray( record.mappingInfo ) ) {
1698
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1699
+ return {
1700
+ ...item,
1701
+ status: 'Closed',
1702
+ };
1703
+ } );
1704
+ }
1705
+ // If no review mapping existed, push a new one
1706
+ if ( record.mappingInfo.length === 0 ) {
1707
+ record.mappingInfo.push( {
1708
+ type: 'approve',
1709
+ mode: inputData.mode,
1710
+ revicedFootfall: revisedFootfall,
1711
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1712
+ count: tempAcc,
1713
+ revisedDetail: formattedTaggingData,
1714
+ status: 'Closed',
1715
+ createdByEmail: req?.user?.email,
1716
+ createdByUserName: req?.user?.userName,
1717
+ createdByRole: req?.user?.role,
1718
+ } );
1719
+ }
1720
+ }
1721
+ record.mappingInfo.push(
1722
+ {
1723
+ type: 'finalRevision',
1724
+ mode: inputData.mode,
1725
+ revicedFootfall: revisedFootfall,
1726
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1727
+ count: tempAcc,
1728
+ revisedDetail: formattedTaggingData,
1729
+ status: 'Closed',
1730
+ createdByEmail: req?.user?.email,
1731
+ createdByUserName: req?.user?.userName,
1732
+ createdByRole: req?.user?.role,
1733
+ createdAt: new Date(),
1734
+ },
1735
+ );
1736
+ }
1737
+ console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
1738
+ let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
1739
+
1740
+
1741
+ if ( checkapprove.length > 0 ) {
1742
+ let userQuery = [
1743
+ {
1744
+ $match: {
1745
+ clientId: getstoreName.clientId,
1746
+ role: 'admin',
1747
+ },
1748
+ },
1749
+ ];
1750
+ let finduserList = await aggregateUser( userQuery );
1751
+ console.log( '🚀 ~ ticketReview ~ finduserList:', finduserList );
1752
+
1753
+ for ( let userData of finduserList ) {
1754
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1755
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1756
+ let description = `Created on ${createdOn}`;
1757
+ console.log( '🚀 ~ ticketCreation ~ userData.role:', userData.email );
1758
+ let Data = {
1759
+ 'title': title,
1760
+ 'body': description,
1761
+ 'type': 'approve',
1762
+ 'date': record.dateString,
1763
+ 'storeId': record.storeId,
1764
+ 'clientId': record.clientId,
1765
+ 'ticketId': record.ticketId,
1766
+ };
1767
+
1768
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1769
+ console.log( ticketsFeature );
1770
+ if ( ticketsFeature ) {
1771
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1772
+ if ( userData && userData.fcmToken && notifyuser ) {
1773
+ const fcmToken = userData.fcmToken;
1774
+ await sendPushNotification( title, description, fcmToken, Data );
1775
+ }
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1781
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1782
+
1783
+ logger.info( { insertResult, record, id } );
1784
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
1785
+ return res.sendSuccess( 'Ticket closed successfully' );
1786
+ } else {
1787
+ return res.sendError( 'Internal Server Error', 500 );
1788
+ }
1789
+ } catch ( error ) {
1790
+ const err = error.message || 'Internal Server Error';
1791
+ logger.error( { error: err, funtion: 'ticketCreation' } );
1792
+ return res.sendError( err, 500 );
1793
+ }
1794
+ }
1795
+
1796
+
1797
+ export async function getAssinedStore( user, storeId ) {
1798
+ if ( user && user.userType === 'client' && user.role !== 'superadmin' ) {
1799
+ let storeIds = new Set( user.assignedStores?.map( ( store ) => store.storeId ) );
1800
+
1801
+ // Fetch clusters and teams in parallel
1802
+ const [ clustersList, teamsList ] = await Promise.all( [
1803
+ findcluster( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } ),
1804
+ findteams( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } ),
1805
+ ] );
1806
+
1807
+ // Process clusters
1808
+ if ( clustersList.length > 0 ) {
1809
+ for ( let cluster of clustersList ) {
1810
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
1811
+ }
1812
+ }
1813
+
1814
+ // Process teams
1815
+ if ( teamsList.length > 0 ) {
1816
+ for ( let team of teamsList ) {
1817
+ for ( let user of team.users ) {
1818
+ let findUser = await findOneUser( { _id: user.userId } );
1819
+ if ( findUser && findUser.assignedStores?.length > 0 ) {
1820
+ findUser.assignedStores.forEach( ( store ) => storeIds.add( store.storeId ) );
1821
+ }
1822
+
1823
+ // Fetch clusters for the user
1824
+ let userClustersList = await findcluster( { clientId: user.clientId, Teamlead: { $elemMatch: { email: findUser.email } } } );
1825
+ if ( userClustersList.length > 0 ) {
1826
+ for ( let cluster of userClustersList ) {
1827
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
1828
+ }
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ let TeamMember = await findteams( { clientId: user.clientId, users: { $elemMatch: { email: user.email } } } );
1834
+ if ( TeamMember && TeamMember.length > 0 ) {
1835
+ for ( let team of TeamMember ) {
1836
+ let clusterList = await findcluster( { clientId: user.clientId, teams: { $elemMatch: { name: team.teamName } } } );
1837
+ if ( clusterList.length > 0 ) {
1838
+ for ( let cluster of clusterList ) {
1839
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
1840
+ }
1841
+ }
1842
+ }
1843
+ }
1844
+ let TeamLeader = await findteams( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } );
1845
+ if ( TeamLeader && TeamLeader.length > 0 ) {
1846
+ for ( let team of TeamLeader ) {
1847
+ let clusterList = await findcluster( { clientId: user.clientId, teams: { $elemMatch: { name: team.teamName } } } );
1848
+ if ( clusterList.length > 0 ) {
1849
+ for ( let cluster of clusterList ) {
1850
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
1851
+ }
1852
+ }
1853
+ }
1854
+ }
1855
+ // Convert Set back to Array if needed
1856
+ let assignedStores = Array.from( storeIds );
1857
+ if ( assignedStores.includes( storeId ) ) {
1858
+ return true;
1859
+ } else {
1860
+ return true;
1861
+ }
1862
+ }
1863
+ }