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

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