tango-app-api-infra 3.9.5-vms.7 → 3.9.5-vms.70

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,117 +416,122 @@ 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 ) {
491
508
  if ( r.actionType === 'reviewer' && r.isChecked === true ) {
492
509
  revisionMapping = {
493
510
  type: 'review',
494
- revicedFootfall: revisedFootfall,
495
- count: getCategory,
511
+ // revicedFootfall: revisedFootfall,
512
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
513
+ count: tempAcc,
496
514
  revisedDetail: formattedTaggingData,
497
- status: 'open',
515
+ status: 'Open',
516
+
498
517
  };
499
518
  } else if ( r.actionType === 'approver' && r.isChecked === true ) {
500
519
  approverMapping = {
501
- type: 'approver',
502
- revicedFootfall: revisedFootfall,
503
- count: getCategory,
520
+ type: 'approve',
521
+ // revicedFootfall: revisedFootfall,
522
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
523
+ count: tempAcc,
504
524
  revisedDetail: formattedTaggingData,
505
- status: 'open',
525
+ status: 'Open',
506
526
  };
507
527
  } else if ( r.actionType === 'tango' && r.isChecked === true ) {
508
528
  tangoReviewMapping = {
509
- type: 'tango-review',
510
- revicedFootfall: revisedFootfall,
511
- count: getCategory,
529
+ type: 'tangoreview',
530
+ // revicedFootfall: revisedFootfall,
531
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
532
+ count: tempAcc,
512
533
  revisedDetail: formattedTaggingData,
513
- status: 'open',
534
+ status: 'Open',
514
535
  };
515
536
  }
516
537
  }
@@ -518,36 +539,179 @@ export async function ticketCreation( req, res, next ) {
518
539
 
519
540
  // Insert appropriate mappingInfo blocks
520
541
  if ( revisionMapping ) {
521
- // If reviewer and checked
542
+ // If reviewer and checked
522
543
  record.mappingInfo.push( revisionMapping );
523
544
  } else if ( approverMapping ) {
524
- // If approver and checked
545
+ // If approver and checked
525
546
  record.mappingInfo.push( approverMapping );
526
547
  } else if ( tangoReviewMapping ) {
527
- // If none above, then tangoReview
548
+ // If none above, then tangoReview
528
549
  record.mappingInfo.push( tangoReviewMapping );
529
550
  }
530
551
  }
531
552
 
553
+ const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
554
+
555
+ const hasReviewer = revision.some(
556
+ ( data ) => data.actionType === 'reviewer' && data.isChecked === true,
557
+ );
558
+ const hasApprover = revision.some(
559
+ ( data ) => data.actionType === 'approver' && data.isChecked === true,
560
+ );
561
+
562
+ if ( hasReviewer || hasApprover ) {
563
+ const userQuery = [
564
+ {
565
+ $match: {
566
+ clientId: getstoreName.clientId,
567
+ role: 'admin',
568
+ isActive: true,
569
+ },
570
+ },
571
+ ];
572
+
573
+ const finduserList = await aggregateUser( userQuery );
574
+
575
+
576
+ const createdOn = dayjs().format( 'DD MMM YYYY' );
577
+ const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
578
+ const description = `Created on ${createdOn}`;
579
+
580
+ const Data = {
581
+ title,
582
+ body: description,
583
+ type: 'create',
584
+ date: record.dateString,
585
+ storeId: record.storeId,
586
+ clientId: record.clientId,
587
+ ticketId: record.ticketId,
588
+ };
589
+
590
+ await Promise.all(
591
+ ( finduserList || [] ).map( async ( userData ) => {
592
+ const ticketsFeature = userData?.rolespermission?.some(
593
+ ( f ) =>
594
+ f.featureName === 'FootfallDirectory' &&
595
+ f.modules?.some(
596
+ ( m ) =>
597
+ m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
598
+ ),
599
+ );
600
+
601
+
602
+ if ( !ticketsFeature ) return;
603
+
604
+ const notifyUser = await getAssinedStore( userData, req.body.storeId );
605
+ if ( !notifyUser || !userData?.fcmToken ) return;
606
+
607
+ await sendPushNotification( title, description, userData.fcmToken, Data );
608
+ } ),
609
+ );
610
+ }
611
+
532
612
 
533
613
  const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
534
614
  const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
535
615
  if ( insertResult && insertResult.statusCode === 201 ) {
536
616
  // 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
617
+
618
+
619
+ const bulkUpdateBody = taggingImages.map( ( img ) => [
620
+ { update: { _index: openSearch.revop, _id: img._id } },
621
+ { doc: { status: 'submitted' } },
622
+ ] ).flat();
623
+
624
+ if ( bulkUpdateBody.length > 0 ) {
625
+ await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
549
626
  }
550
627
 
628
+ if ( record.status = 'Closed' ) {
629
+ const query = {
630
+ storeId: inputData?.storeId,
631
+ isVideoStream: true,
632
+ };
633
+ const getStoreType = await countDocumnetsCamera( query );
634
+ const revopInfoQuery = {
635
+ size: 10000,
636
+ query: {
637
+ bool: {
638
+ must: [
639
+ {
640
+ term: {
641
+ 'storeId.keyword': inputData.storeId,
642
+ },
643
+ },
644
+ {
645
+ term: {
646
+ 'dateString': inputData.dateString,
647
+ },
648
+ },
649
+ {
650
+ term: {
651
+ 'isParent': false,
652
+ },
653
+ },
654
+ {
655
+ term: {
656
+ isChecked: true,
657
+ },
658
+ },
659
+ ],
660
+ },
661
+ },
662
+ _source: [ 'tempId' ],
663
+
664
+ };
665
+
666
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
667
+
668
+ // Get all tempIds from revopInfo response
669
+ const tempIds =
670
+ revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
671
+ // Prepare management eyeZone query based on storeId and dateString
672
+ const managerEyeZoneQuery = {
673
+ size: 1,
674
+ query: {
675
+ bool: {
676
+ must: [
677
+ {
678
+ term: {
679
+ 'storeId.keyword': inputData.storeId,
680
+ },
681
+ },
682
+ {
683
+ term: {
684
+ 'storeDate': inputData.dateString,
685
+ },
686
+ },
687
+ ],
688
+ },
689
+ },
690
+ _source: [ 'originalToTrackerCustomerMapping' ],
691
+ };
692
+
693
+ // Query the managerEyeZone index for the matching document
694
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
695
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
696
+ // Extract originalToTrackerCustomerMapping if it exists
697
+ const mapping =
698
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
699
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
700
+ {};
701
+
702
+ // If you want to compare or find matching tempIds in the mapping
703
+ // The mapping is { "1": tempId1, ... }, so get values as array of tempIds
704
+ // const managerMappedTempIds = Object.values( mapping );
705
+
706
+ // Find tempIds that exist in both revopInfo results and manager mapping
707
+ const temp = [];
708
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
709
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
710
+ if ( isSendMessge == true ) {
711
+ logger.info( '....1' );
712
+ // return true; // res.sendSuccess( 'Ticket has been updated successfully' );
713
+ } // Example: log or use these tempIds for further logic
714
+ }
551
715
  // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
552
716
 
553
717
  if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
@@ -557,7 +721,7 @@ export async function ticketCreation( req, res, next ) {
557
721
  // req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
558
722
  const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
559
723
  const storeId = inputData.storeId;
560
- const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
724
+ const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
561
725
 
562
726
 
563
727
  const formatDate = ( d ) =>
@@ -671,14 +835,1064 @@ export async function ticketCreation( req, res, next ) {
671
835
  }
672
836
  }
673
837
  }
838
+ const sqsName = sqs.vmsPickleExtention;
839
+ const sqsProduceQueue = {
840
+ QueueUrl: `${sqs.url}${sqsName}`,
841
+ MessageBody: JSON.stringify( {
842
+ store_id: inputData?.storeId,
843
+ store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
844
+ primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
845
+ time: new Date(),
846
+ } ),
847
+ MessageGroupId: 'revops-pickle',
848
+ MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
849
+ };
850
+ const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
851
+
852
+ if ( sqsQueue.statusCode ) {
853
+ logger.error( {
854
+ error: `${sqsQueue}`,
855
+ type: 'SQS_NOT_SEND_ERROR',
856
+ } );
857
+ }
858
+
674
859
 
675
860
  return res.sendSuccess( 'Ticket raised successfully' );
676
861
  }
677
862
  } catch ( error ) {
678
863
  const err = error.message || 'Internal Server Error';
679
- logger.error( { error: err, funtion: 'ticketCreation' } );
864
+ logger.error( { error: error, funtion: 'ticketCreation' } );
680
865
  return res.sendError( err, 500 );
681
866
  }
682
867
  }
683
868
 
869
+ export async function ticketReview( req, res, next ) {
870
+ try {
871
+ const inputData = req.body;
872
+ if ( inputData?.type !== 'review' ) {
873
+ return next();
874
+ }
875
+ // check the createtion permission from the user permission
876
+ const userInfo = req?.user;
877
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
878
+ if ( !ticketsFeature ) {
879
+ return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
880
+ }
881
+
882
+ // get store info by the storeId into mongo db
883
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
884
+
885
+ if ( !getstoreName || getstoreName == null ) {
886
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
887
+ }
888
+
889
+ // get the footfall count from opensearch
890
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
891
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
892
+ const getQuery = {
893
+ query: {
894
+ terms: {
895
+ _id: [ dateString ],
896
+ },
897
+ },
898
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
899
+ sort: [
900
+ {
901
+ date_iso: {
902
+ order: 'desc',
903
+ },
904
+ },
905
+ ],
906
+ };
907
+
908
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
909
+ const hits = getFootfallCount?.body?.hits?.hits || [];
910
+ logger.info( { hits } );
911
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
912
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
913
+ }
914
+
915
+ // get category details from the client level configuration
916
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
917
+ if ( !getConfig || getConfig == null ) {
918
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
919
+ }
920
+
921
+ // Get taggingLimitation from config (check both possible paths)
922
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
923
+ // Initialize count object from taggingLimitation
924
+ const tempAcc = [];
925
+ taggingLimitation?.reduce( ( acc, item ) => {
926
+ if ( item?.type ) {
927
+ // Convert type to camelCase with "Count" suffix
928
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
929
+ const typeLower = item.type.toLowerCase();
930
+ let key;
931
+ if ( typeLower === 'housekeeping' ) {
932
+ key = 'houseKeepingCount';
933
+ } else {
934
+ // Convert first letter to lowercase and append "Count"
935
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
936
+ }
937
+
938
+
939
+ // To change from an object to the desired array structure, assemble an array of objects:
940
+ tempAcc.push( {
941
+ name: item.name,
942
+ value: 0,
943
+ key: key,
944
+ type: item.type,
945
+ } );
946
+
947
+
948
+ return acc;
949
+ }
950
+ }, {} ) || {};
951
+
952
+ // Query OpenSearch revop index to get actual counts for each type
953
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
954
+ const revopQuery = {
955
+ size: 0,
956
+ query: {
957
+ bool: {
958
+ must: [
959
+ {
960
+ term: {
961
+ 'storeId.keyword': inputData.storeId,
962
+ },
963
+ },
964
+ {
965
+ term: {
966
+ 'dateString': inputData.dateString,
967
+ },
968
+ },
969
+ {
970
+ term: {
971
+ 'isParent': false,
972
+ },
973
+ },
974
+ {
975
+ term: {
976
+ isChecked: true,
977
+ },
978
+ },
979
+ ],
980
+ },
981
+ },
982
+ aggs: {
983
+ type_counts: {
984
+ terms: {
985
+ field: 'revopsType.keyword',
986
+ size: 100,
987
+ },
988
+ },
989
+ },
990
+ };
991
+
992
+
993
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
994
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
995
+
996
+ // Map OpenSearch revopsType values to count object keys
997
+ buckets.forEach( ( bucket ) => {
998
+ const revopsType = bucket.key;
999
+ const count = bucket.doc_count || 0;
1000
+
1001
+
1002
+ if ( Array.isArray( tempAcc ) ) {
1003
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1004
+ const accMatch = tempAcc.find(
1005
+ ( acc ) =>
1006
+ acc.type &&
1007
+ acc.type === revopsType,
1008
+ );
1009
+
1010
+ if ( accMatch && accMatch.key ) {
1011
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1012
+ }
1013
+ }
1014
+ } );
1015
+ }
1016
+
1017
+
1018
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1019
+
1020
+ const totalCount = Array.isArray( tempAcc ) ?
1021
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1022
+ 0;
1023
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1024
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1025
+ if ( footfallCount - revisedFootfall == 0 ) {
1026
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1027
+ }
1028
+ const taggingData = {
1029
+ size: 10000,
1030
+ query: {
1031
+ bool: {
1032
+ must: [
1033
+ {
1034
+ term: {
1035
+ 'storeId.keyword': inputData.storeId,
1036
+ },
1037
+ },
1038
+ {
1039
+ term: {
1040
+ 'dateString': inputData.dateString,
1041
+ },
1042
+ },
1043
+ ],
1044
+ },
1045
+ },
1046
+ };
1047
+
1048
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1049
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1050
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1051
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1052
+ }
1053
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1054
+
1055
+ const getTicket = {
1056
+ size: 10000,
1057
+ query: {
1058
+ bool: {
1059
+ must: [
1060
+ {
1061
+ term: {
1062
+ 'storeId.keyword': inputData.storeId,
1063
+ },
1064
+ },
1065
+ {
1066
+ term: {
1067
+ 'dateString': inputData.dateString,
1068
+ },
1069
+ },
1070
+ ],
1071
+ },
1072
+ },
1073
+ };
1074
+
1075
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1076
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1077
+ if ( !ticketData || ticketData?.length == 0 ) {
1078
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1079
+ }
1080
+ const record = {
1081
+
1082
+ status: 'Reviewer-Closed',
1083
+ revicedFootfall: revisedFootfall,
1084
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1085
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1086
+ // createdByEmail: req?.user?.email,
1087
+ // createdByUserName: req?.user?.userName,
1088
+ // createdByRole: req?.user?.role,
1089
+
1090
+ };
1091
+
1092
+ if ( Array.isArray( record.mappingInfo ) ) {
1093
+ const temp = record.mappingInfo
1094
+ .filter( ( item ) => item.type === 'review' )
1095
+ .map( ( item ) => ( {
1096
+ ...item,
1097
+ mode: inputData.mode,
1098
+ revicedFootfall: revisedFootfall,
1099
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1100
+ count: tempAcc,
1101
+ revisedDetail: formattedTaggingData,
1102
+ status: 'Closed',
1103
+ createdByEmail: req?.user?.email,
1104
+ createdByUserName: req?.user?.userName,
1105
+ createdByRole: req?.user?.role,
1106
+ createdAt: new Date(),
1107
+ } ) );
1108
+ record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
1109
+ // If no review mapping existed, push a new one
1110
+ if ( record.mappingInfo.length === 0 ) {
1111
+ record.mappingInfo.push( {
1112
+ type: 'review',
1113
+ mode: inputData.mode,
1114
+ revicedFootfall: revisedFootfall,
1115
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1116
+ count: tempAcc,
1117
+ revisedDetail: formattedTaggingData,
1118
+ status: 'Closed',
1119
+ createdByEmail: req?.user?.email,
1120
+ createdByUserName: req?.user?.userName,
1121
+ createdByRole: req?.user?.role,
1122
+ createdAt: new Date(),
1123
+ } );
1124
+ }
1125
+ }
1126
+
1127
+
1128
+ // Retrieve client footfallDirectoryConfigs revision
1129
+ let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
1130
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1131
+
1132
+
1133
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1134
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1135
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1136
+
1137
+
1138
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1139
+ if (
1140
+ isAutoCloseEnable === true &&
1141
+ revisedPercentage >= autoCloseAccuracyValue
1142
+ ) {
1143
+ record.status = 'Reviewer-Closed';
1144
+ // Only keep or modify mappingInfo items with type "review"
1145
+ if ( Array.isArray( record.mappingInfo ) ) {
1146
+ const temp = record.mappingInfo
1147
+ .filter( ( item ) => item.type === 'review' )
1148
+ .map( ( item ) => ( {
1149
+ ...item,
1150
+ mode: inputData.mode,
1151
+ revicedFootfall: revisedFootfall,
1152
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1153
+ count: tempAcc,
1154
+ revisedDetail: formattedTaggingData,
1155
+ status: 'Closed',
1156
+ createdByEmail: req?.user?.email,
1157
+ createdByUserName: req?.user?.userName,
1158
+ createdByRole: req?.user?.role,
1159
+ } ) );
1160
+
1161
+ const temp2 = record.mappingInfo
1162
+ .filter( ( item ) => item.type === 'tagging' )
1163
+ .map( ( item ) => ( {
1164
+ ...item,
1165
+ mode: inputData.mode,
1166
+ // revicedFootfall: revisedFootfall,
1167
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1168
+ // count: tempAcc,
1169
+ // revisedDetail: formattedTaggingData,
1170
+ status: 'Closed',
1171
+ // createdByEmail: req?.user?.email,
1172
+ // createdByUserName: req?.user?.userName,
1173
+ // createdByRole: req?.user?.role,
1174
+ } ) );
1175
+ record.mappingInfo = [ ...temp2, ...temp ];
1176
+ // If no review mapping existed, push a new one
1177
+ if ( record.mappingInfo.length === 0 ) {
1178
+ record.mappingInfo.push( {
1179
+ type: 'review',
1180
+ mode: inputData.mode,
1181
+ revicedFootfall: revisedFootfall,
1182
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1183
+ count: tempAcc,
1184
+ revisedDetail: formattedTaggingData,
1185
+ status: 'Closed',
1186
+ createdByEmail: req?.user?.email,
1187
+ createdByUserName: req?.user?.userName,
1188
+ createdByRole: req?.user?.role,
1189
+ } );
1190
+ }
1191
+ }
1192
+ record.mappingInfo.push(
1193
+ {
1194
+ type: 'finalRevision',
1195
+ mode: inputData.mode,
1196
+ revicedFootfall: revisedFootfall,
1197
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1198
+ count: tempAcc,
1199
+ revisedDetail: formattedTaggingData,
1200
+ status: 'Closed',
1201
+ createdByEmail: req?.user?.email,
1202
+ createdByUserName: req?.user?.userName,
1203
+ createdByRole: req?.user?.role,
1204
+ createdAt: new Date(),
1205
+ },
1206
+ );
1207
+ } else {
1208
+ // If ticket is closed, do not proceed with revision mapping
1209
+ let revisionArray = [];
1210
+
1211
+
1212
+ revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
1213
+
1214
+
1215
+ // Default fallbacks
1216
+ let revisionMapping = null;
1217
+ let approverMapping = null;
1218
+ let tangoReviewMapping = null;
1219
+
1220
+ // Find out which roles have isChecked true
1221
+ if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
1222
+ for ( const r of revisionArray ) {
1223
+ if ( r.actionType === 'approver' && r.isChecked === true ) {
1224
+ approverMapping = {
1225
+ type: 'approve',
1226
+ // revicedFootfall: revisedFootfall,
1227
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1228
+ count: tempAcc,
1229
+ revisedDetail: formattedTaggingData,
1230
+ status: 'Open',
1231
+ };
1232
+ } else if ( r.actionType === 'tango' && r.isChecked === true ) {
1233
+ tangoReviewMapping = {
1234
+ type: 'tangoreview',
1235
+ // revicedFootfall: revisedFootfall,
1236
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1237
+ count: tempAcc,
1238
+ revisedDetail: formattedTaggingData,
1239
+ status: 'Open',
1240
+ };
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ // Insert appropriate mappingInfo blocks
1246
+ if ( revisionMapping ) {
1247
+ // If reviewer and checked
1248
+ record.mappingInfo.push( revisionMapping );
1249
+ } else if ( approverMapping ) {
1250
+ // If approver and checked
1251
+ record.mappingInfo.push( approverMapping );
1252
+ } else if ( tangoReviewMapping ) {
1253
+ // If none above, then tangoReview
1254
+ record.mappingInfo.push( tangoReviewMapping );
1255
+ }
1256
+ }
1257
+
1258
+ let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
1259
+
1260
+
1261
+ if ( checkreview.length > 0 ) {
1262
+ let userQuery = [
1263
+ {
1264
+ $match: {
1265
+ clientId: getstoreName.clientId,
1266
+ role: 'admin',
1267
+ isActive: true,
1268
+ },
1269
+ },
1270
+ ];
1271
+ let finduserList = await aggregateUser( userQuery );
1272
+
1273
+
1274
+ // return;
1275
+ for ( let userData of finduserList ) {
1276
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1277
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1278
+ let description = `Created on ${createdOn}`;
1279
+
1280
+ let Data = {
1281
+ 'title': title,
1282
+ 'body': description,
1283
+ 'type': 'review',
1284
+ 'date': record.dateString,
1285
+ 'storeId': record.storeId,
1286
+ 'clientId': record.clientId,
1287
+ 'ticketId': record.ticketId,
1288
+ };
1289
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1290
+
1291
+ if ( ticketsFeature ) {
1292
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1293
+ if ( userData && userData.fcmToken && notifyuser ) {
1294
+ const fcmToken = userData.fcmToken;
1295
+ await sendPushNotification( title, description, fcmToken, Data );
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+
1301
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1302
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1303
+
1304
+ if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
1305
+ return res.sendSuccess( 'Ticket closed successfully' );
1306
+ } else {
1307
+ return res.sendError( 'Internal Server Error', 500 );
1308
+ }
1309
+ } catch ( error ) {
1310
+ const err = error.message || 'Internal Server Error';
1311
+ logger.error( { error: err, funtion: 'ticketreview' } );
1312
+ return res.sendError( err, 500 );
1313
+ }
1314
+ }
1315
+
1316
+ export async function ticketApprove( req, res, next ) {
1317
+ try {
1318
+ const inputData = req.body;
1319
+ if ( inputData?.type !== 'approve' ) {
1320
+ return next();
1321
+ }
1322
+ // check the createtion permission from the user permission
1323
+ const userInfo = req?.user;
1324
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1325
+ if ( !ticketsFeature ) {
1326
+ return res.sendError( 'Forbidden to Approve this Ticket', 403 );
1327
+ }
1328
+
1329
+ // get store info by the storeId into mongo db
1330
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
1331
+
1332
+ if ( !getstoreName || getstoreName == null ) {
1333
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
1334
+ }
1335
+
1336
+ // get the footfall count from opensearch
1337
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1338
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
1339
+ const getQuery = {
1340
+ query: {
1341
+ terms: {
1342
+ _id: [ dateString ],
1343
+ },
1344
+ },
1345
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
1346
+ sort: [
1347
+ {
1348
+ date_iso: {
1349
+ order: 'desc',
1350
+ },
1351
+ },
1352
+ ],
1353
+ };
1354
+
1355
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
1356
+ const hits = getFootfallCount?.body?.hits?.hits || [];
1357
+ logger.info( { hits } );
1358
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
1359
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
1360
+ }
1361
+
1362
+ // get category details from the client level configuration
1363
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
1364
+ if ( !getConfig || getConfig == null ) {
1365
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
1366
+ }
1367
+
1368
+ // Get taggingLimitation from config (check both possible paths)
1369
+ const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
1370
+ // Initialize count object from taggingLimitation
1371
+ const tempAcc = [];
1372
+ taggingLimitation?.reduce( ( acc, item ) => {
1373
+ if ( item?.type ) {
1374
+ // Convert type to camelCase with "Count" suffix
1375
+ // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
1376
+ const typeLower = item.type.toLowerCase();
1377
+ let key;
1378
+ if ( typeLower === 'housekeeping' ) {
1379
+ key = 'houseKeepingCount';
1380
+ } else {
1381
+ // Convert first letter to lowercase and append "Count"
1382
+ key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
1383
+ }
1384
+
1385
+
1386
+ // To change from an object to the desired array structure, assemble an array of objects:
1387
+ tempAcc.push( {
1388
+ name: item.name,
1389
+ value: 0,
1390
+ key: key,
1391
+ type: item.type,
1392
+ } );
1393
+
1394
+
1395
+ return acc;
1396
+ }
1397
+ }, {} ) || {};
1398
+
1399
+ // Query OpenSearch revop index to get actual counts for each type
1400
+ if ( taggingLimitation && taggingLimitation.length > 0 ) {
1401
+ const revopQuery = {
1402
+ size: 0,
1403
+ query: {
1404
+ bool: {
1405
+ must: [
1406
+ {
1407
+ term: {
1408
+ 'storeId.keyword': inputData.storeId,
1409
+ },
1410
+ },
1411
+ {
1412
+ term: {
1413
+ 'dateString': inputData.dateString,
1414
+ },
1415
+ },
1416
+ ],
1417
+ },
1418
+ },
1419
+ aggs: {
1420
+ type_counts: {
1421
+ terms: {
1422
+ field: 'revopsType.keyword',
1423
+ size: 100,
1424
+ },
1425
+ },
1426
+ },
1427
+ };
1428
+
1429
+
1430
+ const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
1431
+ const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
1432
+
1433
+ // Map OpenSearch revopsType values to count object keys
1434
+ buckets.forEach( ( bucket ) => {
1435
+ const revopsType = bucket.key;
1436
+ const count = bucket.doc_count || 0;
1437
+
1438
+
1439
+ if ( Array.isArray( tempAcc ) ) {
1440
+ // Find the tempAcc entry whose type (case-insensitive) matches revopsType
1441
+ const accMatch = tempAcc.find(
1442
+ ( acc ) =>
1443
+ acc.type &&
1444
+ acc.type === revopsType,
1445
+ );
1446
+
1447
+ if ( accMatch && accMatch.key ) {
1448
+ tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
1449
+ }
1450
+ }
1451
+ } );
1452
+ }
1453
+
1454
+
1455
+ // Calculate revisedFootfall: footfallCount - (sum of all counts)
1456
+
1457
+ const totalCount = Array.isArray( tempAcc ) ?
1458
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
1459
+ 0;
1460
+ const footfallCount = hits?.[0]?._source?.footfall_count || 0;
1461
+ const revisedFootfall = Math.max( 0, footfallCount - totalCount );
1462
+ logger.info( { footfallCount, revisedFootfall } );
1463
+ if ( footfallCount - revisedFootfall == 0 ) {
1464
+ return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
1465
+ }
1466
+
1467
+ const taggingData = {
1468
+ size: 10000,
1469
+ query: {
1470
+ bool: {
1471
+ must: [
1472
+ {
1473
+ term: {
1474
+ 'storeId.keyword': inputData.storeId,
1475
+ },
1476
+ },
1477
+ {
1478
+ term: {
1479
+ 'dateString': inputData.dateString,
1480
+ },
1481
+ },
1482
+ {
1483
+ term: {
1484
+ 'isParent': false,
1485
+ },
1486
+ },
1487
+ {
1488
+ term: {
1489
+ isChecked: true,
1490
+ },
1491
+ },
1492
+ ],
1493
+ },
1494
+ },
1495
+ };
1496
+
1497
+ const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
1498
+ const taggingImages = revopTaggingData?.body?.hits?.hits;
1499
+ if ( !taggingImages || taggingImages?.length == 0 ) {
1500
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1501
+ }
1502
+
1503
+ const formattedTaggingData = formatRevopTaggingHits( taggingImages );
1504
+
1505
+ const getTicket = {
1506
+ size: 10000,
1507
+ query: {
1508
+ bool: {
1509
+ must: [
1510
+ {
1511
+ term: {
1512
+ 'storeId.keyword': inputData.storeId,
1513
+ },
1514
+ },
1515
+ {
1516
+ term: {
1517
+ 'dateString': inputData.dateString,
1518
+ },
1519
+ },
1520
+ ],
1521
+ },
1522
+ },
1523
+ };
1524
+
1525
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
1526
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
1527
+ if ( !ticketData || ticketData?.length == 0 ) {
1528
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
1529
+ }
1530
+ logger.info( { ticketData, getFootfallticketData } );
1531
+ const record = {
1532
+
1533
+ status: 'Approver-Closed',
1534
+ revicedFootfall: revisedFootfall,
1535
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1536
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
1537
+ // createdByEmail: req?.user?.email,
1538
+ // createdByUserName: req?.user?.userName,
1539
+ // createdByRole: req?.user?.role,
1540
+
1541
+ };
1542
+
1543
+
1544
+ // Retrieve client footfallDirectoryConfigs revision
1545
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
1546
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
1547
+
1548
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
1549
+ logger.info( { getNumber } );
1550
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
1551
+ let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
1552
+ const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
1553
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
1554
+ logger.info( { tangoReview, revised } );
1555
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
1556
+ if (
1557
+ isAutoCloseEnable === true &&
1558
+ revisedPercentage >= autoCloseAccuracyValue
1559
+ ) {
1560
+ record.status = 'Approver-Closed';
1561
+ // Only keep or modify mappingInfo items with type "review"
1562
+ if ( Array.isArray( record.mappingInfo ) ) {
1563
+ const temp = record.mappingInfo
1564
+ .filter( ( item ) => item.type === 'approve' )
1565
+ .map( ( item ) => ( {
1566
+ ...item,
1567
+
1568
+ mode: inputData.mode,
1569
+ revicedFootfall: revisedFootfall,
1570
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1571
+ count: tempAcc,
1572
+ revisedDetail: formattedTaggingData,
1573
+ status: 'Closed',
1574
+ createdByEmail: req?.user?.email,
1575
+ createdByUserName: req?.user?.userName,
1576
+ createdByRole: req?.user?.role,
1577
+ } ) );
1578
+
1579
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1580
+ ...temp ];
1581
+ // If updating the mapping config to mark [i].status as 'Closed'
1582
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
1583
+ if ( Array.isArray( record.mappingInfo ) ) {
1584
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1585
+ return {
1586
+ ...item,
1587
+ status: 'Closed',
1588
+ };
1589
+ } );
1590
+ }
1591
+ // If no review mapping existed, push a new one
1592
+ if ( record.mappingInfo.length === 0 ) {
1593
+ record.mappingInfo.push( {
1594
+ type: 'approve',
1595
+ mode: inputData.mode,
1596
+ revicedFootfall: revisedFootfall,
1597
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1598
+ count: tempAcc,
1599
+ revisedDetail: formattedTaggingData,
1600
+ status: 'Closed',
1601
+ createdByEmail: req?.user?.email,
1602
+ createdByUserName: req?.user?.userName,
1603
+ createdByRole: req?.user?.role,
1604
+ } );
1605
+ }
1606
+ }
1607
+ record.mappingInfo.push(
1608
+ {
1609
+ type: 'finalRevision',
1610
+ mode: inputData.mode,
1611
+ revicedFootfall: revisedFootfall,
1612
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1613
+ count: tempAcc,
1614
+ revisedDetail: formattedTaggingData,
1615
+ status: 'Closed',
1616
+ createdByEmail: req?.user?.email,
1617
+ createdByUserName: req?.user?.userName,
1618
+ createdByRole: req?.user?.role,
1619
+ createdAt: new Date(),
1620
+ },
1621
+ );
1622
+ } else if ( revised < tangoReview ) {
1623
+ // If ticket is closed, do not proceed with revision mapping
1624
+
1625
+ // Default fallbacks
1626
+
1627
+ let approverMapping = null;
1628
+ let tangoReviewMapping = null;
1629
+
1630
+ record.status = 'Approver-Closed';
1631
+ // Only keep or modify mappingInfo items with type "review"
1632
+ if ( Array.isArray( record.mappingInfo ) ) {
1633
+ const temp = record.mappingInfo
1634
+ .filter( ( item ) => item.type === 'approve' )
1635
+ .map( ( item ) => ( {
1636
+ ...item,
1637
+ mode: inputData.mode,
1638
+ revicedFootfall: revisedFootfall,
1639
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1640
+ count: tempAcc,
1641
+ revisedDetail: formattedTaggingData,
1642
+ status: 'Closed',
1643
+ createdByEmail: req?.user?.email,
1644
+ createdByUserName: req?.user?.userName,
1645
+ createdByRole: req?.user?.role,
1646
+ } ) );
1647
+
1648
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1649
+ ...temp ];
1650
+ // if ( Array.isArray( record.mappingInfo ) ) {
1651
+ // record.mappingInfo = record.mappingInfo.map( ( item ) => {
1652
+ // return {
1653
+ // ...item,
1654
+ // status: 'Closed',
1655
+ // };
1656
+ // } );
1657
+ // }
1658
+ // If no review mapping existed, push a new one
1659
+ if ( record.mappingInfo.length === 0 ) {
1660
+ record.mappingInfo.push( {
1661
+ type: 'approve',
1662
+ mode: inputData.mode,
1663
+ revicedFootfall: revisedFootfall,
1664
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1665
+ count: tempAcc,
1666
+ revisedDetail: formattedTaggingData,
1667
+ status: 'Closed',
1668
+ createdByEmail: req?.user?.email,
1669
+ createdByUserName: req?.user?.userName,
1670
+ createdByRole: req?.user?.role,
1671
+ } );
1672
+ }
1673
+ }
1674
+
1675
+ // Find out which roles have isChecked true
1676
+
1677
+ // for ( const r of revisionArray ) {
1678
+ // if ( r.actionType === 'tango' && r.isChecked === true ) {
1679
+ tangoReviewMapping = {
1680
+ type: 'tangoreview',
1681
+ // revicedFootfall: revisedFootfall,
1682
+ // revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1683
+ count: tempAcc,
1684
+ revisedDetail: formattedTaggingData,
1685
+ status: 'Open',
1686
+ };
1687
+ // }
1688
+ // }
1689
+
1690
+
1691
+ if ( approverMapping ) {
1692
+ // If approver and checked
1693
+ record.mappingInfo.push( approverMapping );
1694
+ } else if ( tangoReviewMapping ) {
1695
+ // If none above, then tangoReview
1696
+ record.mappingInfo.push( tangoReviewMapping );
1697
+ }
1698
+ } else {
1699
+ if ( Array.isArray( record.mappingInfo ) ) {
1700
+ const temp = record.mappingInfo
1701
+ .filter( ( item ) => item.type === 'approve' )
1702
+ .map( ( item ) => ( {
1703
+ ...item,
1704
+ mode: inputData.mode,
1705
+ revicedFootfall: revisedFootfall,
1706
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1707
+ count: tempAcc,
1708
+ revisedDetail: formattedTaggingData,
1709
+ status: 'Closed',
1710
+ createdByEmail: req?.user?.email,
1711
+ createdByUserName: req?.user?.userName,
1712
+ createdByRole: req?.user?.role,
1713
+ } ) );
1714
+
1715
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
1716
+ ...temp ];
1717
+ if ( Array.isArray( record.mappingInfo ) ) {
1718
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
1719
+ return {
1720
+ ...item,
1721
+ status: 'Closed',
1722
+ };
1723
+ } );
1724
+ }
1725
+ // If no review mapping existed, push a new one
1726
+ if ( record.mappingInfo.length === 0 ) {
1727
+ record.mappingInfo.push( {
1728
+ type: 'approve',
1729
+ mode: inputData.mode,
1730
+ revicedFootfall: revisedFootfall,
1731
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1732
+ count: tempAcc,
1733
+ revisedDetail: formattedTaggingData,
1734
+ status: 'Closed',
1735
+ createdByEmail: req?.user?.email,
1736
+ createdByUserName: req?.user?.userName,
1737
+ createdByRole: req?.user?.role,
1738
+ } );
1739
+ }
1740
+ }
1741
+ record.mappingInfo.push(
1742
+ {
1743
+ type: 'finalRevision',
1744
+ mode: inputData.mode,
1745
+ revicedFootfall: revisedFootfall,
1746
+ revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
1747
+ count: tempAcc,
1748
+ revisedDetail: formattedTaggingData,
1749
+ status: 'Closed',
1750
+ createdByEmail: req?.user?.email,
1751
+ createdByUserName: req?.user?.userName,
1752
+ createdByRole: req?.user?.role,
1753
+ createdAt: new Date(),
1754
+ },
1755
+ );
1756
+ }
1757
+ console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
1758
+ let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
1759
+
1760
+
1761
+ if ( checkapprove.length > 0 ) {
1762
+ let userQuery = [
1763
+ {
1764
+ $match: {
1765
+ clientId: getstoreName.clientId,
1766
+ role: 'admin',
1767
+ isActive: true,
1768
+ },
1769
+ },
1770
+ ];
1771
+ let finduserList = await aggregateUser( userQuery );
1772
+
1773
+
1774
+ for ( let userData of finduserList ) {
1775
+ let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
1776
+ let createdOn = dayjs().format( 'DD MMM YYYY' );
1777
+ let description = `Created on ${createdOn}`;
1778
+
1779
+ let Data = {
1780
+ 'title': title,
1781
+ 'body': description,
1782
+ 'type': 'approve',
1783
+ 'date': record.dateString,
1784
+ 'storeId': record.storeId,
1785
+ 'clientId': record.clientId,
1786
+ 'ticketId': record.ticketId,
1787
+ };
1788
+
1789
+ const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1790
+
1791
+ if ( ticketsFeature ) {
1792
+ let notifyuser = await getAssinedStore( userData, req.body.storeId );
1793
+ if ( userData && userData.fcmToken && notifyuser ) {
1794
+ const fcmToken = userData.fcmToken;
1795
+ await sendPushNotification( title, description, fcmToken, Data );
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1800
+
1801
+ const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
1802
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
1803
+
1804
+
1805
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
1806
+ return res.sendSuccess( 'Ticket closed successfully' );
1807
+ } else {
1808
+ return res.sendError( 'Internal Server Error', 500 );
1809
+ }
1810
+ } catch ( error ) {
1811
+ const err = error.message || 'Internal Server Error';
1812
+ logger.error( { error: err, funtion: 'ticketCreation' } );
1813
+ return res.sendError( err, 500 );
1814
+ }
1815
+ }
1816
+
1817
+ export async function getAssinedStore( user, storeId ) {
1818
+ if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
1819
+ return;
1820
+ }
1821
+
1822
+ const clientId = user.clientId;
1823
+ const storeIds = new Set(
1824
+ user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
1825
+ );
1826
+
1827
+ const addClusterStores = ( clusters ) => {
1828
+ if ( !clusters?.length ) return;
1829
+ for ( const cluster of clusters ) {
1830
+ cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
1831
+ }
1832
+ };
1833
+
1834
+ // Fetch all top-level data in parallel
1835
+ const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
1836
+ findcluster( {
1837
+ clientId,
1838
+ Teamlead: { $elemMatch: { email: user.email } },
1839
+ } ),
1840
+ findteams( {
1841
+ clientId,
1842
+ Teamlead: { $elemMatch: { email: user.email } },
1843
+ } ),
1844
+ findteams( {
1845
+ clientId,
1846
+ users: { $elemMatch: { email: user.email } },
1847
+ } ),
1848
+ ] );
1849
+
1850
+ // 1) Clusters where this user is Teamlead
1851
+ addClusterStores( clustersList );
1852
+
1853
+ // 2) Teams where this user is Teamlead → their users + their clusters
1854
+ if ( teamsList?.length ) {
1855
+ for ( const team of teamsList ) {
1856
+ if ( !team.users?.length ) continue;
1857
+
1858
+ await Promise.all(
1859
+ team.users.map( async ( teamUser ) => {
1860
+ const foundUser = await findOneUser( { _id: teamUser.userId } );
1861
+ if ( !foundUser ) return;
1862
+
1863
+ // Direct assigned stores of that user
1864
+ if ( foundUser.assignedStores?.length ) {
1865
+ foundUser.assignedStores.forEach( ( store ) =>
1866
+ storeIds.add( store.storeId ),
1867
+ );
1868
+ }
1869
+
1870
+ // Clusters where this user is Teamlead
1871
+ const userClustersList = await findcluster( {
1872
+ clientId,
1873
+ Teamlead: { $elemMatch: { email: foundUser.email } },
1874
+ } );
1875
+ addClusterStores( userClustersList );
1876
+ } ),
1877
+ );
1878
+ }
1879
+ }
1880
+
1881
+ // 3) Teams where this user is a member → clusters by teamName
1882
+ if ( teamMemberList?.length ) {
1883
+ for ( const team of teamMemberList ) {
1884
+ const clusterList = await findcluster( {
1885
+ clientId,
1886
+ teams: { $elemMatch: { name: team.teamName } },
1887
+ } );
1888
+ addClusterStores( clusterList );
1889
+ }
1890
+ }
1891
+
1892
+ const assignedStores = Array.from( storeIds );
1893
+
1894
+ // Previously you returned `true` in both branches.
1895
+ // Assuming you actually want to check membership:
1896
+ return assignedStores.includes( storeId );
1897
+ }
684
1898