tango-app-api-infra 3.9.7 → 3.9.9
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.
- package/package.json +5 -4
- package/src/controllers/footfallDirectory.controllers.js +5677 -922
- package/src/controllers/infra.controllers.js +34 -21
- package/src/dtos/footfallDirectory.dtos.js +233 -125
- package/src/routes/footfallDirectory.routes.js +22 -6
- package/src/services/storeAccuracyIssues.service.js +9 -0
- package/src/services/vmsStoreRequest.service.js +4 -0
- package/src/validations/footfallDirectory.validation.js +2614 -7
|
@@ -1,10 +1,71 @@
|
|
|
1
|
-
import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
|
|
1
|
+
import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger, sendMessageToFIFOQueue, updateOpenSearchData, getOpenSearchById } from 'tango-app-api-middleware';
|
|
2
2
|
import { aggregateCluster } from '../services/cluster.service.js';
|
|
3
3
|
import { findOneRevopDownload } from '../services/revopDownload.service.js';
|
|
4
|
+
import { findOneStore } from '../services/store.service.js';
|
|
5
|
+
import { aggregateClient, findOneClient } from '../services/client.service.js';
|
|
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';
|
|
16
|
+
|
|
17
|
+
function formatRevopTaggingHits( hits = [] ) {
|
|
18
|
+
return hits
|
|
19
|
+
.map( ( hit ) => {
|
|
20
|
+
const source = hit?._source || {};
|
|
21
|
+
if ( Object.keys( source ).length === 0 ) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const duplicateImages = Array.isArray( source.duplicateImage ) ?
|
|
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
|
+
[];
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: source?.id,
|
|
41
|
+
clientId: source?.clientId,
|
|
42
|
+
storeId: source?.storeId,
|
|
43
|
+
tempId: source?.tempId,
|
|
44
|
+
dateString: source?.dateString,
|
|
45
|
+
timeRange: source?.timeRange,
|
|
46
|
+
processType: source?.processType,
|
|
47
|
+
revopsType: source?.revopsType,
|
|
48
|
+
entryTime: source?.entryTime,
|
|
49
|
+
exitTime: source?.exitTime,
|
|
50
|
+
filePath: source?.filePath,
|
|
51
|
+
status: source?.status,
|
|
52
|
+
description: source?.description || '',
|
|
53
|
+
isChecked: Boolean( source?.isChecked ),
|
|
54
|
+
type: source?.type,
|
|
55
|
+
action: source?.action,
|
|
56
|
+
parent: source?.parent ?? null,
|
|
57
|
+
isParent: duplicateImages.length > 0 && !source?.parent,
|
|
58
|
+
createdAt: source?.createdAt,
|
|
59
|
+
updatedAt: source?.updatedAt,
|
|
60
|
+
duplicateImage: duplicateImages,
|
|
61
|
+
};
|
|
62
|
+
} )
|
|
63
|
+
.filter( Boolean );
|
|
64
|
+
}
|
|
4
65
|
|
|
5
66
|
export async function isExist( req, res, next ) {
|
|
6
67
|
try {
|
|
7
|
-
const inputData=req.body;
|
|
68
|
+
const inputData = req.body;
|
|
8
69
|
const opensearch = JSON.parse( process.env.OPENSEARCH );
|
|
9
70
|
const query = {
|
|
10
71
|
query: {
|
|
@@ -26,8 +87,8 @@ export async function isExist( req, res, next ) {
|
|
|
26
87
|
};
|
|
27
88
|
|
|
28
89
|
const getData = await getOpenSearchCount( opensearch.footfallDirectory, query );
|
|
29
|
-
const isExist = getData?.body?.count == 0? true : false;
|
|
30
|
-
|
|
90
|
+
const isExist = getData?.body?.count == 0 ? true : false;
|
|
91
|
+
|
|
31
92
|
if ( isExist === true ) {
|
|
32
93
|
next();
|
|
33
94
|
} else {
|
|
@@ -42,12 +103,12 @@ export async function isExist( req, res, next ) {
|
|
|
42
103
|
|
|
43
104
|
export async function getClusters( req, res, next ) {
|
|
44
105
|
try {
|
|
45
|
-
const inputData=req.query;
|
|
106
|
+
const inputData = req.query;
|
|
46
107
|
// const assignedStores = req.body.assignedStores;
|
|
47
108
|
inputData.clientId = inputData?.clientId?.split( ',' );
|
|
48
109
|
const clusters = inputData?.clusters?.split( ',' ); // convert strig to array
|
|
49
110
|
// logger.info( { assignedStores, clusters } );
|
|
50
|
-
let filter =[
|
|
111
|
+
let filter = [
|
|
51
112
|
{
|
|
52
113
|
clientId: { $in: inputData.clientId },
|
|
53
114
|
},
|
|
@@ -93,7 +154,7 @@ export async function getClusters( req, res, next ) {
|
|
|
93
154
|
if ( getStores?.[0]?.stores?.length == 0 ) {
|
|
94
155
|
return res.sendError( 'No data', 204 );
|
|
95
156
|
}
|
|
96
|
-
|
|
157
|
+
|
|
97
158
|
req.stores = getStores?.[0]?.stores;
|
|
98
159
|
return next();
|
|
99
160
|
} catch ( error ) {
|
|
@@ -129,3 +190,2549 @@ export async function isTicketExists( req, res, next ) {
|
|
|
129
190
|
}
|
|
130
191
|
}
|
|
131
192
|
|
|
193
|
+
export async function isGrantedUsers( req, res, next ) {
|
|
194
|
+
try {
|
|
195
|
+
const userInfo = req?.user;
|
|
196
|
+
switch ( userInfo.userType ) {
|
|
197
|
+
case 'client':
|
|
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 ) ) );
|
|
199
|
+
|
|
200
|
+
if ( ticketsFeature ) {
|
|
201
|
+
return next();
|
|
202
|
+
} else {
|
|
203
|
+
return res.sendError( 'Forbidden to this action', 403 );
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case 'tango':
|
|
207
|
+
return next();
|
|
208
|
+
default:
|
|
209
|
+
return res.sendError( 'userType doesnot match', 400 );
|
|
210
|
+
}
|
|
211
|
+
} catch ( error ) {
|
|
212
|
+
const err = error.message || 'Internal Server Error';
|
|
213
|
+
return res.sendError( err, 500 );
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function getConfig( req, res, next ) {
|
|
218
|
+
try {
|
|
219
|
+
const inputData = req.body;
|
|
220
|
+
const storeKey = inputData.storeId.split( '-' )[0];
|
|
221
|
+
|
|
222
|
+
const config = await findOneClient( { clientId: storeKey }, { footfallDirectoryConfigs: 1 } );
|
|
223
|
+
const accuracyBreach = config?.footfallDirectoryConfigs?.accuracyBreach;
|
|
224
|
+
req.accuracyBreach = accuracyBreach || '';
|
|
225
|
+
return next();
|
|
226
|
+
} catch ( error ) {
|
|
227
|
+
const err = error.message || 'Internal Server Error';
|
|
228
|
+
return res.sendError( err, 500 );
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
export async function ticketCreation( req, res, next ) {
|
|
234
|
+
try {
|
|
235
|
+
const inputData = req.body;
|
|
236
|
+
const sqs = JSON.parse( process.env.SQS );
|
|
237
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
238
|
+
if ( inputData?.type !== 'create' ) {
|
|
239
|
+
return next();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const query = {
|
|
243
|
+
query: {
|
|
244
|
+
bool: {
|
|
245
|
+
must: [
|
|
246
|
+
{
|
|
247
|
+
term: {
|
|
248
|
+
'storeId.keyword': inputData?.storeId,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
term: {
|
|
253
|
+
'dateString': inputData?.dateString,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
term: {
|
|
258
|
+
'type': 'store',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const getData = await getOpenSearchCount( openSearch.footfallDirectory, query );
|
|
267
|
+
const isExist = getData?.body?.count == 0 ? true : false;
|
|
268
|
+
|
|
269
|
+
if ( isExist !== true ) {
|
|
270
|
+
return res.sendError( 'The ticket is already exist', 403 );
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
// get store info by the storeId into mongo db
|
|
275
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
276
|
+
|
|
277
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
278
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// get category details from the client level configuration
|
|
282
|
+
const configQuery = [
|
|
283
|
+
{
|
|
284
|
+
$match: {
|
|
285
|
+
clientId: getstoreName?.clientId,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// Convert all effectiveFrom to proper Date
|
|
290
|
+
{
|
|
291
|
+
$addFields: {
|
|
292
|
+
taggingLimitationWithDate: {
|
|
293
|
+
$map: {
|
|
294
|
+
input: '$footfallDirectoryConfigs.taggingLimitation',
|
|
295
|
+
as: 'item',
|
|
296
|
+
in: {
|
|
297
|
+
effectiveFrom: { $toDate: '$$item.effectiveFrom' },
|
|
298
|
+
values: '$$item.values',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
// Filter items <= input date
|
|
306
|
+
{
|
|
307
|
+
$addFields: {
|
|
308
|
+
matchedLimitation: {
|
|
309
|
+
$filter: {
|
|
310
|
+
input: '$taggingLimitationWithDate',
|
|
311
|
+
as: 'item',
|
|
312
|
+
cond: {
|
|
313
|
+
$lte: [
|
|
314
|
+
'$$item.effectiveFrom',
|
|
315
|
+
{ $toDate: inputData.dateString },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Sort DESC and pick ONLY top 1 -> latest effective record
|
|
324
|
+
{
|
|
325
|
+
$addFields: {
|
|
326
|
+
effectiveLimitation: {
|
|
327
|
+
$arrayElemAt: [
|
|
328
|
+
{
|
|
329
|
+
$slice: [
|
|
330
|
+
{
|
|
331
|
+
$sortArray: {
|
|
332
|
+
input: '$matchedLimitation',
|
|
333
|
+
sortBy: { effectiveFrom: -1 },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
1,
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
0,
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
{
|
|
346
|
+
$project: {
|
|
347
|
+
config: 1,
|
|
348
|
+
effectiveLimitation: 1,
|
|
349
|
+
footfallDirectoryConfigs: 1,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
const config = await aggregateClient( configQuery );
|
|
356
|
+
const getConfig = config[0];
|
|
357
|
+
if ( !getConfig || getConfig == null ) {
|
|
358
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
359
|
+
}
|
|
360
|
+
const today = dayjs();
|
|
361
|
+
const diff = today.diff( inputData.dateString, 'day' );
|
|
362
|
+
const taggingDueDate = getConfig?.footfallDirectoryConfigs?.allowTicketCreation || 0;
|
|
363
|
+
if ( diff > taggingDueDate ) {
|
|
364
|
+
return res.sendError( 'Ticket Creation is not allowed beyond the due date', 400 );
|
|
365
|
+
}
|
|
366
|
+
// check the createtion permission from the user permission
|
|
367
|
+
const userInfo = req?.user;
|
|
368
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
369
|
+
if ( !ticketsFeature ) {
|
|
370
|
+
return res.sendError( 'Forbidden to Create Ticket', 403 );
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
// get the footfall count from opensearch
|
|
375
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
376
|
+
const getQuery = {
|
|
377
|
+
query: {
|
|
378
|
+
terms: {
|
|
379
|
+
_id: [ dateString ],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
383
|
+
sort: [
|
|
384
|
+
{
|
|
385
|
+
date_iso: {
|
|
386
|
+
order: 'desc',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
393
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
394
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
395
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
400
|
+
const taggingLimitation = getConfig?.effectiveLimitation?.values;
|
|
401
|
+
// Initialize count object from taggingLimitation
|
|
402
|
+
|
|
403
|
+
const tempAcc = [];
|
|
404
|
+
taggingLimitation.reduce( ( acc, item ) => {
|
|
405
|
+
if ( item?.type ) {
|
|
406
|
+
// Convert type to camelCase with "Count" suffix
|
|
407
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
408
|
+
const typeLower = item.type.toLowerCase();
|
|
409
|
+
let key;
|
|
410
|
+
if ( typeLower === 'housekeeping' ) {
|
|
411
|
+
key = 'houseKeepingCount';
|
|
412
|
+
} else {
|
|
413
|
+
// Convert first letter to lowercase and append "Count"
|
|
414
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
419
|
+
tempAcc.push( {
|
|
420
|
+
name: item.name,
|
|
421
|
+
value: 0,
|
|
422
|
+
key: key,
|
|
423
|
+
type: item.type,
|
|
424
|
+
} );
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
return acc;
|
|
428
|
+
}
|
|
429
|
+
}, {} ) || {};
|
|
430
|
+
|
|
431
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
432
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
433
|
+
const revopQuery = {
|
|
434
|
+
size: 0,
|
|
435
|
+
query: {
|
|
436
|
+
bool: {
|
|
437
|
+
must: [
|
|
438
|
+
{
|
|
439
|
+
term: {
|
|
440
|
+
'storeId.keyword': inputData.storeId,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
term: {
|
|
445
|
+
'dateString': inputData.dateString,
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
term: {
|
|
450
|
+
'isParent': false,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
aggs: {
|
|
457
|
+
type_counts: {
|
|
458
|
+
terms: {
|
|
459
|
+
field: 'revopsType.keyword',
|
|
460
|
+
size: 50000,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
468
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
469
|
+
// Map OpenSearch revopsType values to count object keys
|
|
470
|
+
buckets.forEach( ( bucket ) => {
|
|
471
|
+
const revopsType = bucket.key;
|
|
472
|
+
const count = bucket.doc_count || 0;
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
476
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
477
|
+
const accMatch = tempAcc.find(
|
|
478
|
+
( acc ) =>
|
|
479
|
+
acc.type &&
|
|
480
|
+
acc.type === revopsType,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
if ( accMatch && accMatch.key ) {
|
|
484
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} );
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
492
|
+
|
|
493
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
494
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
495
|
+
0;
|
|
496
|
+
|
|
497
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
498
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
499
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
500
|
+
return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const taggingData = {
|
|
504
|
+
size: 50000,
|
|
505
|
+
query: {
|
|
506
|
+
bool: {
|
|
507
|
+
must: [
|
|
508
|
+
{
|
|
509
|
+
term: {
|
|
510
|
+
'storeId.keyword': inputData.storeId,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
term: {
|
|
515
|
+
'dateString': inputData.dateString,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
524
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
525
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
526
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
527
|
+
}
|
|
528
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
529
|
+
|
|
530
|
+
const record = {
|
|
531
|
+
storeId: inputData.storeId,
|
|
532
|
+
type: 'store',
|
|
533
|
+
dateString: inputData.dateString,
|
|
534
|
+
storeName: getstoreName?.storeName,
|
|
535
|
+
ticketName: inputData.ticketName || 'footfall-directory',
|
|
536
|
+
footfallCount: footfallCount,
|
|
537
|
+
clientId: getstoreName?.clientId,
|
|
538
|
+
ticketId: 'TE_FDT_' + new Date().valueOf(),
|
|
539
|
+
createdAt: new Date(),
|
|
540
|
+
updatedAt: new Date(),
|
|
541
|
+
status: 'Raised',
|
|
542
|
+
comments: inputData?.comments || '',
|
|
543
|
+
revicedFootfall: revisedFootfall,
|
|
544
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
545
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
546
|
+
mappingInfo: [
|
|
547
|
+
{
|
|
548
|
+
type: 'tagging',
|
|
549
|
+
mode: inputData.mode,
|
|
550
|
+
revicedFootfall: revisedFootfall,
|
|
551
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
552
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
553
|
+
count: tempAcc,
|
|
554
|
+
revisedDetail: formattedTaggingData,
|
|
555
|
+
status: 'Raised',
|
|
556
|
+
createdByEmail: req?.user?.email,
|
|
557
|
+
createdByUserName: req?.user?.userName,
|
|
558
|
+
createdByRole: req?.user?.role,
|
|
559
|
+
createdAt: new Date(),
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
566
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
567
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
|
|
568
|
+
|
|
569
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
570
|
+
|
|
571
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
572
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
573
|
+
|
|
574
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
575
|
+
if (
|
|
576
|
+
isAutoCloseEnable === true &&
|
|
577
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
578
|
+
) {
|
|
579
|
+
record.status = 'Closed';
|
|
580
|
+
record.mappingInfo = [
|
|
581
|
+
{
|
|
582
|
+
type: 'tagging',
|
|
583
|
+
mode: inputData.mode,
|
|
584
|
+
revicedFootfall: revisedFootfall,
|
|
585
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
586
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
587
|
+
count: tempAcc,
|
|
588
|
+
revisedDetail: formattedTaggingData,
|
|
589
|
+
status: 'Closed',
|
|
590
|
+
createdByEmail: req?.user?.email,
|
|
591
|
+
createdByUserName: req?.user?.userName,
|
|
592
|
+
createdByRole: req?.user?.role,
|
|
593
|
+
updatedAt: new Date(),
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: 'finalRevision',
|
|
597
|
+
mode: inputData.mode,
|
|
598
|
+
revicedFootfall: revisedFootfall,
|
|
599
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
600
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
601
|
+
count: tempAcc,
|
|
602
|
+
revisedDetail: formattedTaggingData,
|
|
603
|
+
status: 'Closed',
|
|
604
|
+
createdByEmail: req?.user?.email,
|
|
605
|
+
createdByUserName: req?.user?.userName,
|
|
606
|
+
createdByRole: req?.user?.role,
|
|
607
|
+
updatedAt: new Date(),
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
} else {
|
|
611
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
612
|
+
let revisionArray = [];
|
|
613
|
+
|
|
614
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
615
|
+
const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
|
|
616
|
+
const approverDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoApprove || 0;
|
|
617
|
+
const reviwerDueDate = getConfig?.footfallDirectoryConfigs?.allowTicketReview || 0;
|
|
618
|
+
// Default fallbacks
|
|
619
|
+
let revisionMapping = null;
|
|
620
|
+
let approverMapping = null;
|
|
621
|
+
let tangoReviewMapping = null;
|
|
622
|
+
// Find out which roles have isChecked true
|
|
623
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
624
|
+
for ( const r of revisionArray ) {
|
|
625
|
+
if ( r.actionType === 'reviewer' && r.isChecked === true ) {
|
|
626
|
+
revisionMapping = {
|
|
627
|
+
type: 'review',
|
|
628
|
+
count: tempAcc,
|
|
629
|
+
revisedDetail: formattedTaggingData,
|
|
630
|
+
status: 'Open',
|
|
631
|
+
dueDate: new Date( Date.now() + reviwerDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
632
|
+
createdAt: new Date(),
|
|
633
|
+
updatedAt: new Date(),
|
|
634
|
+
|
|
635
|
+
};
|
|
636
|
+
} else if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
637
|
+
approverMapping = {
|
|
638
|
+
type: 'approve',
|
|
639
|
+
count: tempAcc,
|
|
640
|
+
revisedDetail: formattedTaggingData,
|
|
641
|
+
status: 'Open',
|
|
642
|
+
dueDate: new Date( Date.now() + approverDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
643
|
+
createdAt: new Date(),
|
|
644
|
+
updatedAt: new Date(),
|
|
645
|
+
};
|
|
646
|
+
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
647
|
+
tangoReviewMapping = {
|
|
648
|
+
type: 'tangoreview',
|
|
649
|
+
count: tempAcc,
|
|
650
|
+
revisedDetail: formattedTaggingData,
|
|
651
|
+
status: 'Open',
|
|
652
|
+
dueDate: new Date( Date.now() + tangoDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
653
|
+
createdAt: new Date(),
|
|
654
|
+
updatedAt: new Date(),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Insert appropriate mappingInfo blocks
|
|
661
|
+
if ( revisionMapping ) {
|
|
662
|
+
// If reviewer and checked
|
|
663
|
+
record.mappingInfo.push( revisionMapping );
|
|
664
|
+
} else if ( approverMapping ) {
|
|
665
|
+
// If approver and checked
|
|
666
|
+
record.mappingInfo.push( approverMapping );
|
|
667
|
+
} else if ( tangoReviewMapping ) {
|
|
668
|
+
// If none above, then tangoReview
|
|
669
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
|
|
674
|
+
|
|
675
|
+
const hasReviewer = revision.some(
|
|
676
|
+
( data ) => data.actionType === 'reviewer' && data.isChecked === true,
|
|
677
|
+
);
|
|
678
|
+
const hasApprover = revision.some(
|
|
679
|
+
( data ) => data.actionType === 'approver' && data.isChecked === true,
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
if ( hasReviewer || hasApprover ) {
|
|
683
|
+
const userQuery = [
|
|
684
|
+
{
|
|
685
|
+
$match: {
|
|
686
|
+
clientId: getstoreName.clientId,
|
|
687
|
+
role: 'admin',
|
|
688
|
+
isActive: true,
|
|
689
|
+
appName: 'tangoeye',
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
];
|
|
693
|
+
|
|
694
|
+
const finduserList = await aggregateUser( userQuery );
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
const createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
698
|
+
const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
699
|
+
const description = `Created on ${createdOn}`;
|
|
700
|
+
|
|
701
|
+
const Data = {
|
|
702
|
+
title,
|
|
703
|
+
body: description,
|
|
704
|
+
type: 'create',
|
|
705
|
+
date: record.dateString,
|
|
706
|
+
storeId: record.storeId,
|
|
707
|
+
clientId: record.clientId,
|
|
708
|
+
ticketId: record.ticketId,
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
await Promise.all(
|
|
712
|
+
( finduserList || [] ).map( async ( userData ) => {
|
|
713
|
+
const ticketsFeature = userData?.rolespermission?.some(
|
|
714
|
+
( f ) =>
|
|
715
|
+
f.featureName === 'FootfallDirectory' &&
|
|
716
|
+
f.modules?.some(
|
|
717
|
+
( m ) =>
|
|
718
|
+
m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
|
|
719
|
+
),
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
if ( !ticketsFeature ) return;
|
|
724
|
+
|
|
725
|
+
const notifyUser = await getAssinedStore( userData, req.body.storeId );
|
|
726
|
+
if ( !notifyUser || !userData?.fcmToken ) return;
|
|
727
|
+
|
|
728
|
+
await sendPushNotification( title, description, userData.fcmToken, Data );
|
|
729
|
+
} ),
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
735
|
+
const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
|
|
736
|
+
if ( insertResult && insertResult.statusCode === 201 ) {
|
|
737
|
+
// After successful ticket creation, update status to "submitted" in revop index for the relevant records
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
const bulkUpdateBody = taggingImages.map( ( img ) => [
|
|
741
|
+
{ update: { _index: openSearch.revop, _id: img._id } },
|
|
742
|
+
{ doc: { status: 'submitted' } },
|
|
743
|
+
] ).flat();
|
|
744
|
+
|
|
745
|
+
if ( bulkUpdateBody.length > 0 ) {
|
|
746
|
+
await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if ( record.status === 'Closed' ) {
|
|
750
|
+
const query = {
|
|
751
|
+
storeId: inputData?.storeId,
|
|
752
|
+
isVideoStream: true,
|
|
753
|
+
};
|
|
754
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
755
|
+
const revopInfoQuery = {
|
|
756
|
+
size: 50000,
|
|
757
|
+
query: {
|
|
758
|
+
bool: {
|
|
759
|
+
must: [
|
|
760
|
+
{
|
|
761
|
+
term: {
|
|
762
|
+
'storeId.keyword': inputData.storeId,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
term: {
|
|
767
|
+
'dateString': inputData.dateString,
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
term: {
|
|
772
|
+
'isParent': false,
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
_source: [ 'tempId' ],
|
|
779
|
+
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
783
|
+
|
|
784
|
+
// Get all tempIds from revopInfo response
|
|
785
|
+
const tempIds =
|
|
786
|
+
revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
787
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
788
|
+
const managerEyeZoneQuery = {
|
|
789
|
+
size: 1,
|
|
790
|
+
query: {
|
|
791
|
+
bool: {
|
|
792
|
+
must: [
|
|
793
|
+
{
|
|
794
|
+
term: {
|
|
795
|
+
'storeId.keyword': inputData.storeId,
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
term: {
|
|
800
|
+
'zoneId.keyword': 'Overall Store',
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
term: {
|
|
805
|
+
'storeDate': inputData.dateString,
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
],
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Query the managerEyeZone index for the matching document
|
|
816
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
817
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
818
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
819
|
+
const mapping =
|
|
820
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
821
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
822
|
+
{ tempId: '' };
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
826
|
+
const temp = [];
|
|
827
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) : '' );
|
|
828
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
829
|
+
if ( isSendMessge == true ) {
|
|
830
|
+
logger.info( '....1' );
|
|
831
|
+
// return true; // res.sendSuccess( 'Ticket has been updated successfully' );
|
|
832
|
+
} // Example: log or use these tempIds for further logic
|
|
833
|
+
}
|
|
834
|
+
// Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
|
|
835
|
+
|
|
836
|
+
if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
|
|
837
|
+
const breachDays = Number( req.accuracyBreach.days );
|
|
838
|
+
const breachCount = Number( req.accuracyBreach.ticketCount );
|
|
839
|
+
|
|
840
|
+
// req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
|
|
841
|
+
const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
|
|
842
|
+
const storeId = inputData.storeId;
|
|
843
|
+
const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
const formatDate = ( d ) =>
|
|
847
|
+
`${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
|
|
848
|
+
|
|
849
|
+
// Compute current date object
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
// Calculate start date based on config days
|
|
853
|
+
// If 30 days: start from first day of current month
|
|
854
|
+
// If 60 days: start from first day of last month
|
|
855
|
+
const currentDateObj = new Date(); // Declare currentDateObj as today's date
|
|
856
|
+
const startDateObj = new Date( currentDateObj );
|
|
857
|
+
|
|
858
|
+
if ( breachDays === 30 ) {
|
|
859
|
+
// Consider within this month
|
|
860
|
+
startDateObj.setDate( 1 ); // First day of current month
|
|
861
|
+
} else if ( breachDays === 60 ) {
|
|
862
|
+
// Consider this month and last month
|
|
863
|
+
startDateObj.setMonth( startDateObj.getMonth() - 1 );
|
|
864
|
+
startDateObj.setDate( 1 ); // First day of last month
|
|
865
|
+
} else {
|
|
866
|
+
// For other values, calculate months from days
|
|
867
|
+
const breachMonths = Math.ceil( breachDays / 30 );
|
|
868
|
+
startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
|
|
869
|
+
startDateObj.setDate( 1 );
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
const startDate = startDateObj;
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
const endDate = new Date;
|
|
877
|
+
// Query for tickets within this months window for this store and ticket name
|
|
878
|
+
const query = {
|
|
879
|
+
query: {
|
|
880
|
+
bool: {
|
|
881
|
+
must: [
|
|
882
|
+
{ term: { 'storeId.keyword': storeId } },
|
|
883
|
+
{ term: { 'ticketName.keyword': ticketName } },
|
|
884
|
+
{ range: { createdAt: { gte: startDate, lte: endDate } } },
|
|
885
|
+
],
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// Search in OpenSearch for recent similar tickets
|
|
891
|
+
const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
|
|
892
|
+
const tickets = breachTicketsResult?.body?.hits?.hits || [];
|
|
893
|
+
|
|
894
|
+
// Filter tickets where revised footfall percentage > config accuracy
|
|
895
|
+
let breachTicketsCount = 0;
|
|
896
|
+
for ( const ticket of tickets ) {
|
|
897
|
+
const source = ticket._source || {};
|
|
898
|
+
const footfallCount = source.footfallCount || 0;
|
|
899
|
+
const revicedFootfall = source.mappingInfo?.[0]?.revicedFootfall || 0;
|
|
900
|
+
|
|
901
|
+
// Calculate revised footfall percentage
|
|
902
|
+
let revisedPercentage = 0;
|
|
903
|
+
if ( footfallCount > 0 ) {
|
|
904
|
+
revisedPercentage = ( revicedFootfall / footfallCount ) * 100;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Check if revised footfall percentage > config accuracy
|
|
908
|
+
if ( revisedPercentage > breachAccuracy ) {
|
|
909
|
+
breachTicketsCount++;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if ( breachTicketsCount >= breachCount ) {
|
|
914
|
+
// Calculate remaining future days in the config period
|
|
915
|
+
const futureDates = [];
|
|
916
|
+
|
|
917
|
+
// Calculate end date of config period
|
|
918
|
+
const configEndDateObj = new Date( currentDateObj );
|
|
919
|
+
if ( breachDays === 30 ) {
|
|
920
|
+
// End of current month
|
|
921
|
+
configEndDateObj.setMonth( configEndDateObj.getMonth() + 1 );
|
|
922
|
+
configEndDateObj.setDate( 0 ); // Last day of current month
|
|
923
|
+
} else if ( breachDays === 60 ) {
|
|
924
|
+
// End of next month
|
|
925
|
+
configEndDateObj.setMonth( configEndDateObj.getMonth() + 2 );
|
|
926
|
+
configEndDateObj.setDate( 0 ); // Last day of next month
|
|
927
|
+
} else {
|
|
928
|
+
// For other values, add the remaining days
|
|
929
|
+
const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
|
|
930
|
+
configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Generate all dates from tomorrow until end of config period
|
|
934
|
+
const tomorrow = new Date( currentDateObj );
|
|
935
|
+
tomorrow.setDate( tomorrow.getDate() + 0 );
|
|
936
|
+
|
|
937
|
+
const dateIterator = new Date( tomorrow );
|
|
938
|
+
while ( dateIterator <= configEndDateObj ) {
|
|
939
|
+
futureDates.push( formatDate( dateIterator ) );
|
|
940
|
+
dateIterator.setDate( dateIterator.getDate() + 1 );
|
|
941
|
+
}
|
|
942
|
+
// Also check the past 3 days; for each day, if footfallDirectory index does not have a record for this storeId/dateString/type:"store", block that day too
|
|
943
|
+
// const missingPastDays = [];
|
|
944
|
+
for ( let delta = -3; delta < 0; delta++ ) {
|
|
945
|
+
const dateObj = new Date( currentDateObj );
|
|
946
|
+
dateObj.setDate( dateObj.getDate() + delta );
|
|
947
|
+
const dateStr = formatDate( dateObj );
|
|
948
|
+
// Query OpenSearch for this day
|
|
949
|
+
const pastQuery = {
|
|
950
|
+
query: {
|
|
951
|
+
bool: {
|
|
952
|
+
must: [
|
|
953
|
+
{ term: { 'storeId.keyword': inputData?.storeId } },
|
|
954
|
+
{ term: { dateString: dateStr } },
|
|
955
|
+
{ term: { 'type.keyword': 'store' } },
|
|
956
|
+
],
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
// Use getOpenSearchData to check if exists
|
|
961
|
+
let found = false;
|
|
962
|
+
|
|
963
|
+
const searchRes = await getOpenSearchData( openSearch.footfallDirectory, pastQuery );
|
|
964
|
+
const foundHits = searchRes?.body?.hits?.hits || [];
|
|
965
|
+
|
|
966
|
+
if ( foundHits.length > 0 ) {
|
|
967
|
+
found = true;
|
|
968
|
+
} else {
|
|
969
|
+
found = false;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if ( !found ) {
|
|
973
|
+
const record = {
|
|
974
|
+
clientId: getstoreName?.clientId,
|
|
975
|
+
storeId: inputData?.storeId,
|
|
976
|
+
storeName: getstoreName?.storeName,
|
|
977
|
+
dateString: dateStr,
|
|
978
|
+
status: 'block',
|
|
979
|
+
};
|
|
980
|
+
await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: dateStr }, record );
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
// Insert a record for each future date
|
|
986
|
+
for ( const futureDateString of futureDates ) {
|
|
987
|
+
const record = {
|
|
988
|
+
clientId: getstoreName?.clientId,
|
|
989
|
+
storeId: inputData?.storeId,
|
|
990
|
+
storeName: getstoreName?.storeName,
|
|
991
|
+
dateString: futureDateString,
|
|
992
|
+
status: 'block',
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const sqsName = sqs.vmsPickleExtention;
|
|
1000
|
+
const sqsProduceQueue = {
|
|
1001
|
+
QueueUrl: `${sqs.url}${sqsName}`,
|
|
1002
|
+
MessageBody: JSON.stringify( {
|
|
1003
|
+
store_id: inputData?.storeId,
|
|
1004
|
+
store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
|
|
1005
|
+
primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
1006
|
+
time: new Date(),
|
|
1007
|
+
} ),
|
|
1008
|
+
MessageGroupId: 'revops-pickle',
|
|
1009
|
+
MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
1010
|
+
};
|
|
1011
|
+
const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
|
|
1012
|
+
|
|
1013
|
+
if ( sqsQueue.statusCode ) {
|
|
1014
|
+
logger.error( {
|
|
1015
|
+
error: `${sqsQueue}`,
|
|
1016
|
+
type: 'SQS_NOT_SEND_ERROR',
|
|
1017
|
+
} );
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return res.sendSuccess( 'Ticket raised successfully' );
|
|
1021
|
+
}
|
|
1022
|
+
} catch ( error ) {
|
|
1023
|
+
const err = error.message || 'Internal Server Error';
|
|
1024
|
+
logger.error( { error: error, funtion: 'ticketCreation' } );
|
|
1025
|
+
return res.sendError( err, 500 );
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export async function ticketReview( req, res, next ) {
|
|
1030
|
+
try {
|
|
1031
|
+
const inputData = req.body;
|
|
1032
|
+
if ( inputData?.type !== 'review' ) {
|
|
1033
|
+
return next();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// check the createtion permission from the user permission
|
|
1037
|
+
const userInfo = req?.user;
|
|
1038
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1039
|
+
|
|
1040
|
+
if ( !ticketsFeature ) {
|
|
1041
|
+
return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// get store info by the storeId into mongo db
|
|
1045
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
1046
|
+
|
|
1047
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
1048
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// get the footfall count from opensearch
|
|
1052
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1053
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
1054
|
+
const getQuery = {
|
|
1055
|
+
query: {
|
|
1056
|
+
terms: {
|
|
1057
|
+
_id: [ dateString ],
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
1061
|
+
sort: [
|
|
1062
|
+
{
|
|
1063
|
+
date_iso: {
|
|
1064
|
+
order: 'desc',
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
1071
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
1072
|
+
|
|
1073
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
1074
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// get category details from the client level configuration
|
|
1078
|
+
const configQuery = [
|
|
1079
|
+
{
|
|
1080
|
+
$match: {
|
|
1081
|
+
clientId: getstoreName?.clientId,
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
|
|
1085
|
+
// Convert all effectiveFrom to proper Date
|
|
1086
|
+
{
|
|
1087
|
+
$addFields: {
|
|
1088
|
+
taggingLimitationWithDate: {
|
|
1089
|
+
$map: {
|
|
1090
|
+
input: '$footfallDirectoryConfigs.taggingLimitation',
|
|
1091
|
+
as: 'item',
|
|
1092
|
+
in: {
|
|
1093
|
+
effectiveFrom: { $toDate: '$$item.effectiveFrom' },
|
|
1094
|
+
values: '$$item.values',
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
|
|
1101
|
+
// Filter items <= input date
|
|
1102
|
+
{
|
|
1103
|
+
$addFields: {
|
|
1104
|
+
matchedLimitation: {
|
|
1105
|
+
$filter: {
|
|
1106
|
+
input: '$taggingLimitationWithDate',
|
|
1107
|
+
as: 'item',
|
|
1108
|
+
cond: {
|
|
1109
|
+
$lte: [
|
|
1110
|
+
'$$item.effectiveFrom',
|
|
1111
|
+
{ $toDate: inputData.dateString },
|
|
1112
|
+
],
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
|
|
1119
|
+
// Sort DESC and pick ONLY top 1 -> latest effective record
|
|
1120
|
+
{
|
|
1121
|
+
$addFields: {
|
|
1122
|
+
effectiveLimitation: {
|
|
1123
|
+
$arrayElemAt: [
|
|
1124
|
+
{
|
|
1125
|
+
$slice: [
|
|
1126
|
+
{
|
|
1127
|
+
$sortArray: {
|
|
1128
|
+
input: '$matchedLimitation',
|
|
1129
|
+
sortBy: { effectiveFrom: -1 },
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
1,
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
0,
|
|
1136
|
+
],
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
{
|
|
1142
|
+
$project: {
|
|
1143
|
+
config: 1,
|
|
1144
|
+
effectiveLimitation: 1,
|
|
1145
|
+
footfallDirectoryConfigs: 1,
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
];
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
const config = await aggregateClient( configQuery );
|
|
1152
|
+
const getConfig = config[0];
|
|
1153
|
+
if ( !getConfig || getConfig == null ) {
|
|
1154
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
1159
|
+
const taggingLimitation = getConfig?.effectiveLimitation?.values;
|
|
1160
|
+
// Initialize count object from taggingLimitation
|
|
1161
|
+
const tempAcc = [];
|
|
1162
|
+
taggingLimitation.reduce( ( acc, item ) => {
|
|
1163
|
+
if ( item?.type ) {
|
|
1164
|
+
// Convert type to camelCase with "Count" suffix
|
|
1165
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
1166
|
+
const typeLower = item.type.toLowerCase();
|
|
1167
|
+
let key;
|
|
1168
|
+
if ( typeLower === 'housekeeping' ) {
|
|
1169
|
+
key = 'houseKeepingCount';
|
|
1170
|
+
} else {
|
|
1171
|
+
// Convert first letter to lowercase and append "Count"
|
|
1172
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
1177
|
+
tempAcc.push( {
|
|
1178
|
+
name: item.name,
|
|
1179
|
+
value: 0,
|
|
1180
|
+
key: key,
|
|
1181
|
+
type: item.type,
|
|
1182
|
+
} );
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
return acc;
|
|
1186
|
+
}
|
|
1187
|
+
}, {} ) || {};
|
|
1188
|
+
|
|
1189
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
1190
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
1191
|
+
const revopQuery = {
|
|
1192
|
+
size: 0,
|
|
1193
|
+
query: {
|
|
1194
|
+
bool: {
|
|
1195
|
+
must: [
|
|
1196
|
+
{
|
|
1197
|
+
term: {
|
|
1198
|
+
'storeId.keyword': inputData.storeId,
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
term: {
|
|
1203
|
+
'dateString': inputData.dateString,
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
term: {
|
|
1208
|
+
'isParent': false,
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
term: {
|
|
1213
|
+
isChecked: true,
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
aggs: {
|
|
1220
|
+
type_counts: {
|
|
1221
|
+
terms: {
|
|
1222
|
+
field: 'revopsType.keyword',
|
|
1223
|
+
size: 100,
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
1231
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
1232
|
+
|
|
1233
|
+
// Map OpenSearch revopsType values to count object keys
|
|
1234
|
+
buckets.forEach( ( bucket ) => {
|
|
1235
|
+
const revopsType = bucket.key;
|
|
1236
|
+
const count = bucket.doc_count || 0;
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1240
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1241
|
+
const accMatch = tempAcc.find(
|
|
1242
|
+
( acc ) =>
|
|
1243
|
+
acc.type &&
|
|
1244
|
+
acc.type === revopsType,
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
if ( accMatch && accMatch.key ) {
|
|
1248
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} );
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1256
|
+
|
|
1257
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1258
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1259
|
+
0;
|
|
1260
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1261
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1262
|
+
// if ( footfallCount - revisedFootfall == 0 ) {
|
|
1263
|
+
// return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1264
|
+
// }
|
|
1265
|
+
|
|
1266
|
+
const taggingData = {
|
|
1267
|
+
size: 50000,
|
|
1268
|
+
query: {
|
|
1269
|
+
bool: {
|
|
1270
|
+
must: [
|
|
1271
|
+
{
|
|
1272
|
+
term: {
|
|
1273
|
+
'storeId.keyword': inputData.storeId,
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
term: {
|
|
1278
|
+
'dateString': inputData.dateString,
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
],
|
|
1282
|
+
},
|
|
1283
|
+
},
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1287
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1288
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1289
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1290
|
+
}
|
|
1291
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1292
|
+
|
|
1293
|
+
const getTicket = {
|
|
1294
|
+
size: 50000,
|
|
1295
|
+
query: {
|
|
1296
|
+
bool: {
|
|
1297
|
+
must: [
|
|
1298
|
+
{
|
|
1299
|
+
term: {
|
|
1300
|
+
'type.keyword': 'store',
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
term: {
|
|
1305
|
+
'storeId.keyword': inputData.storeId,
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
term: {
|
|
1310
|
+
'dateString': inputData.dateString,
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
],
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1319
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1320
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1321
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1322
|
+
}
|
|
1323
|
+
const reviewDueDate = ticketData?.[0]?._source?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate;
|
|
1324
|
+
|
|
1325
|
+
if ( dayjs().isAfter( dayjs( reviewDueDate ), 'day' ) ) {
|
|
1326
|
+
return res.sendError( 'Ticket Review is not allowed beyond the due date', 400 );
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const record = {
|
|
1330
|
+
|
|
1331
|
+
status: 'Reviewer-Closed',
|
|
1332
|
+
revicedFootfall: revisedFootfall,
|
|
1333
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1334
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1335
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1336
|
+
// createdByEmail: req?.user?.email,
|
|
1337
|
+
// createdByUserName: req?.user?.userName,
|
|
1338
|
+
// createdByRole: req?.user?.role,
|
|
1339
|
+
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1343
|
+
const temp = record.mappingInfo
|
|
1344
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1345
|
+
.map( ( item ) => ( {
|
|
1346
|
+
...item,
|
|
1347
|
+
mode: inputData.mode,
|
|
1348
|
+
revicedFootfall: revisedFootfall,
|
|
1349
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1350
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1351
|
+
count: tempAcc,
|
|
1352
|
+
revisedDetail: formattedTaggingData,
|
|
1353
|
+
status: 'Closed',
|
|
1354
|
+
createdByEmail: req?.user?.email,
|
|
1355
|
+
createdByUserName: req?.user?.userName,
|
|
1356
|
+
createdByRole: req?.user?.role,
|
|
1357
|
+
} ) );
|
|
1358
|
+
record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
|
|
1359
|
+
// If no review mapping existed, push a new one
|
|
1360
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1361
|
+
record.mappingInfo.push( {
|
|
1362
|
+
type: 'review',
|
|
1363
|
+
mode: inputData.mode,
|
|
1364
|
+
revicedFootfall: revisedFootfall,
|
|
1365
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1366
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1367
|
+
count: tempAcc,
|
|
1368
|
+
revisedDetail: formattedTaggingData,
|
|
1369
|
+
status: 'Closed',
|
|
1370
|
+
createdByEmail: req?.user?.email,
|
|
1371
|
+
createdByUserName: req?.user?.userName,
|
|
1372
|
+
createdByRole: req?.user?.role,
|
|
1373
|
+
} );
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1379
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
1380
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1384
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1385
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1386
|
+
const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
|
|
1387
|
+
const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
|
|
1388
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1389
|
+
if (
|
|
1390
|
+
isAutoCloseEnable === true &&
|
|
1391
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1392
|
+
) {
|
|
1393
|
+
record.status = 'Closed';
|
|
1394
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1395
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1396
|
+
const temp = record.mappingInfo
|
|
1397
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1398
|
+
.map( ( item ) => ( {
|
|
1399
|
+
...item,
|
|
1400
|
+
mode: inputData.mode,
|
|
1401
|
+
revicedFootfall: revisedFootfall,
|
|
1402
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1403
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1404
|
+
count: tempAcc,
|
|
1405
|
+
revisedDetail: formattedTaggingData,
|
|
1406
|
+
status: 'Closed',
|
|
1407
|
+
createdByEmail: req?.user?.email,
|
|
1408
|
+
createdByUserName: req?.user?.userName,
|
|
1409
|
+
createdByRole: req?.user?.role,
|
|
1410
|
+
} ) );
|
|
1411
|
+
|
|
1412
|
+
const temp2 = record.mappingInfo
|
|
1413
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1414
|
+
.map( ( item ) => ( {
|
|
1415
|
+
...item,
|
|
1416
|
+
mode: inputData.mode,
|
|
1417
|
+
|
|
1418
|
+
status: 'Closed',
|
|
1419
|
+
} ) );
|
|
1420
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1421
|
+
// If no review mapping existed, push a new one
|
|
1422
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1423
|
+
record.mappingInfo.push( {
|
|
1424
|
+
type: 'review',
|
|
1425
|
+
mode: inputData.mode,
|
|
1426
|
+
revicedFootfall: revisedFootfall,
|
|
1427
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1428
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1429
|
+
count: tempAcc,
|
|
1430
|
+
revisedDetail: formattedTaggingData,
|
|
1431
|
+
status: 'Closed',
|
|
1432
|
+
createdByEmail: req?.user?.email,
|
|
1433
|
+
createdByUserName: req?.user?.userName,
|
|
1434
|
+
createdByRole: req?.user?.role,
|
|
1435
|
+
} );
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
record.mappingInfo.push(
|
|
1439
|
+
{
|
|
1440
|
+
type: 'finalRevision',
|
|
1441
|
+
mode: inputData.mode,
|
|
1442
|
+
revicedFootfall: revisedFootfall,
|
|
1443
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1444
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1445
|
+
count: tempAcc,
|
|
1446
|
+
revisedDetail: formattedTaggingData,
|
|
1447
|
+
status: 'Closed',
|
|
1448
|
+
createdByEmail: req?.user?.email,
|
|
1449
|
+
createdByUserName: req?.user?.userName,
|
|
1450
|
+
createdByRole: req?.user?.role,
|
|
1451
|
+
createdAt: new Date(),
|
|
1452
|
+
updatedAt: new Date(),
|
|
1453
|
+
},
|
|
1454
|
+
);
|
|
1455
|
+
} else {
|
|
1456
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1457
|
+
let revisionArray = [];
|
|
1458
|
+
|
|
1459
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
1460
|
+
|
|
1461
|
+
const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
|
|
1462
|
+
const approverDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoApprove || 0;
|
|
1463
|
+
const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
|
|
1464
|
+
// Default fallbacks
|
|
1465
|
+
let revisionMapping = null;
|
|
1466
|
+
let approverMapping = null;
|
|
1467
|
+
let tangoReviewMapping = null;
|
|
1468
|
+
|
|
1469
|
+
// Find out which roles have isChecked true
|
|
1470
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
1471
|
+
for ( const r of revisionArray ) {
|
|
1472
|
+
if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
1473
|
+
approverMapping = {
|
|
1474
|
+
type: 'approve',
|
|
1475
|
+
count: tempAcc,
|
|
1476
|
+
revisedDetail: formattedTaggingData,
|
|
1477
|
+
status: 'Open',
|
|
1478
|
+
dueDate: new Date( Date.now() + approverDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1479
|
+
createdAt: new Date(),
|
|
1480
|
+
updatedAt: new Date(),
|
|
1481
|
+
};
|
|
1482
|
+
} else if ( r.actionType === 'approver' && r.isChecked === false ) {
|
|
1483
|
+
if ( revised < tangoReview ) {
|
|
1484
|
+
if ( tangoApproved ) {
|
|
1485
|
+
record.status = 'Reviewer-Closed';
|
|
1486
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1487
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1488
|
+
const temp = record.mappingInfo
|
|
1489
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1490
|
+
.map( ( item ) => ( {
|
|
1491
|
+
...item,
|
|
1492
|
+
mode: inputData.mode,
|
|
1493
|
+
revicedFootfall: revisedFootfall,
|
|
1494
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1495
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1496
|
+
count: tempAcc,
|
|
1497
|
+
revisedDetail: formattedTaggingData,
|
|
1498
|
+
status: 'Under Tango Review',
|
|
1499
|
+
createdByEmail: req?.user?.email,
|
|
1500
|
+
createdByUserName: req?.user?.userName,
|
|
1501
|
+
createdByRole: req?.user?.role,
|
|
1502
|
+
updatedAt: new Date(),
|
|
1503
|
+
} ) );
|
|
1504
|
+
|
|
1505
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1506
|
+
...temp ];
|
|
1507
|
+
|
|
1508
|
+
// If no review mapping existed, push a new one
|
|
1509
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1510
|
+
record.mappingInfo.push( {
|
|
1511
|
+
type: 'review',
|
|
1512
|
+
mode: inputData.mode,
|
|
1513
|
+
revicedFootfall: revisedFootfall,
|
|
1514
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1515
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1516
|
+
count: tempAcc,
|
|
1517
|
+
revisedDetail: formattedTaggingData,
|
|
1518
|
+
status: 'Under Tango Review',
|
|
1519
|
+
createdByEmail: req?.user?.email,
|
|
1520
|
+
createdByUserName: req?.user?.userName,
|
|
1521
|
+
createdByRole: req?.user?.role,
|
|
1522
|
+
updatedAt: new Date(),
|
|
1523
|
+
} );
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Find out which roles have isChecked true
|
|
1528
|
+
|
|
1529
|
+
// for ( const r of revisionArray ) {
|
|
1530
|
+
// if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1531
|
+
tangoReviewMapping = {
|
|
1532
|
+
type: 'tangoreview',
|
|
1533
|
+
count: tempAcc,
|
|
1534
|
+
revisedDetail: formattedTaggingData,
|
|
1535
|
+
status: 'Open',
|
|
1536
|
+
dueDate: new Date( Date.now() + tangoDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1537
|
+
createdAt: new Date(),
|
|
1538
|
+
updatedAt: new Date(),
|
|
1539
|
+
};
|
|
1540
|
+
// }
|
|
1541
|
+
// }
|
|
1542
|
+
} else {
|
|
1543
|
+
record.status ='Closed';
|
|
1544
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1545
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1546
|
+
const temp = record.mappingInfo
|
|
1547
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1548
|
+
.map( ( item ) => ( {
|
|
1549
|
+
...item,
|
|
1550
|
+
mode: inputData.mode,
|
|
1551
|
+
revicedFootfall: revisedFootfall,
|
|
1552
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1553
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1554
|
+
count: tempAcc,
|
|
1555
|
+
revisedDetail: formattedTaggingData,
|
|
1556
|
+
status: 'Closed',
|
|
1557
|
+
createdByEmail: req?.user?.email,
|
|
1558
|
+
createdByUserName: req?.user?.userName,
|
|
1559
|
+
createdByRole: req?.user?.role,
|
|
1560
|
+
} ) );
|
|
1561
|
+
|
|
1562
|
+
const temp2 = record.mappingInfo
|
|
1563
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1564
|
+
.map( ( item ) => ( {
|
|
1565
|
+
...item,
|
|
1566
|
+
mode: inputData.mode,
|
|
1567
|
+
status: 'Closed',
|
|
1568
|
+
|
|
1569
|
+
} ) );
|
|
1570
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1571
|
+
// If no review mapping existed, push a new one
|
|
1572
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1573
|
+
record.mappingInfo.push( {
|
|
1574
|
+
type: 'review',
|
|
1575
|
+
mode: inputData.mode,
|
|
1576
|
+
revicedFootfall: revisedFootfall,
|
|
1577
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1578
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1579
|
+
count: tempAcc,
|
|
1580
|
+
revisedDetail: formattedTaggingData,
|
|
1581
|
+
status: 'Closed',
|
|
1582
|
+
createdByEmail: req?.user?.email,
|
|
1583
|
+
createdByUserName: req?.user?.userName,
|
|
1584
|
+
createdByRole: req?.user?.role,
|
|
1585
|
+
} );
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
record.mappingInfo.push(
|
|
1589
|
+
{
|
|
1590
|
+
type: 'finalRevision',
|
|
1591
|
+
mode: inputData.mode,
|
|
1592
|
+
revicedFootfall: revisedFootfall,
|
|
1593
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1594
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1595
|
+
count: tempAcc,
|
|
1596
|
+
revisedDetail: formattedTaggingData,
|
|
1597
|
+
status: 'Closed',
|
|
1598
|
+
createdByEmail: req?.user?.email,
|
|
1599
|
+
createdByUserName: req?.user?.userName,
|
|
1600
|
+
createdByRole: req?.user?.role,
|
|
1601
|
+
createdAt: new Date(),
|
|
1602
|
+
updatedAt: new Date(),
|
|
1603
|
+
},
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
tangoReviewMapping = {
|
|
1607
|
+
type: 'tangoreview',
|
|
1608
|
+
count: tempAcc,
|
|
1609
|
+
revisedDetail: formattedTaggingData,
|
|
1610
|
+
status: 'Open',
|
|
1611
|
+
dueDate: new Date( Date.now() + tangoDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1612
|
+
createdAt: new Date(),
|
|
1613
|
+
updatedAt: new Date(),
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
} else {
|
|
1617
|
+
record.status ='Closed';
|
|
1618
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1619
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1620
|
+
const temp = record.mappingInfo
|
|
1621
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1622
|
+
.map( ( item ) => ( {
|
|
1623
|
+
...item,
|
|
1624
|
+
mode: inputData.mode,
|
|
1625
|
+
revicedFootfall: revisedFootfall,
|
|
1626
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1627
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1628
|
+
count: tempAcc,
|
|
1629
|
+
revisedDetail: formattedTaggingData,
|
|
1630
|
+
status: 'Closed',
|
|
1631
|
+
createdByEmail: req?.user?.email,
|
|
1632
|
+
createdByUserName: req?.user?.userName,
|
|
1633
|
+
createdByRole: req?.user?.role,
|
|
1634
|
+
} ) );
|
|
1635
|
+
|
|
1636
|
+
const temp2 = record.mappingInfo
|
|
1637
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1638
|
+
.map( ( item ) => ( {
|
|
1639
|
+
...item,
|
|
1640
|
+
mode: inputData.mode,
|
|
1641
|
+
status: 'Closed',
|
|
1642
|
+
|
|
1643
|
+
} ) );
|
|
1644
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1645
|
+
// If no review mapping existed, push a new one
|
|
1646
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1647
|
+
record.mappingInfo.push( {
|
|
1648
|
+
type: 'review',
|
|
1649
|
+
mode: inputData.mode,
|
|
1650
|
+
revicedFootfall: revisedFootfall,
|
|
1651
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1652
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1653
|
+
count: tempAcc,
|
|
1654
|
+
revisedDetail: formattedTaggingData,
|
|
1655
|
+
status: 'Closed',
|
|
1656
|
+
createdByEmail: req?.user?.email,
|
|
1657
|
+
createdByUserName: req?.user?.userName,
|
|
1658
|
+
createdByRole: req?.user?.role,
|
|
1659
|
+
} );
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
record.mappingInfo.push(
|
|
1663
|
+
{
|
|
1664
|
+
type: 'finalRevision',
|
|
1665
|
+
mode: inputData.mode,
|
|
1666
|
+
revicedFootfall: revisedFootfall,
|
|
1667
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1668
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1669
|
+
count: tempAcc,
|
|
1670
|
+
revisedDetail: formattedTaggingData,
|
|
1671
|
+
status: 'Closed',
|
|
1672
|
+
createdByEmail: req?.user?.email,
|
|
1673
|
+
createdByUserName: req?.user?.userName,
|
|
1674
|
+
createdByRole: req?.user?.role,
|
|
1675
|
+
createdAt: new Date(),
|
|
1676
|
+
updatedAt: new Date(),
|
|
1677
|
+
},
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// Insert appropriate mappingInfo blocks
|
|
1684
|
+
if ( revisionMapping ) {
|
|
1685
|
+
// If reviewer and checked
|
|
1686
|
+
record.mappingInfo.push( revisionMapping );
|
|
1687
|
+
} else if ( approverMapping ) {
|
|
1688
|
+
// If approver and checked
|
|
1689
|
+
record.mappingInfo.push( approverMapping );
|
|
1690
|
+
} else if ( tangoReviewMapping ) {
|
|
1691
|
+
// If none above, then tangoReview
|
|
1692
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
if ( checkreview.length > 0 && record.status === 'Reviewer-Closed' ) {
|
|
1700
|
+
let userQuery = [
|
|
1701
|
+
{
|
|
1702
|
+
$match: {
|
|
1703
|
+
clientId: getstoreName.clientId,
|
|
1704
|
+
role: 'admin',
|
|
1705
|
+
isActive: true,
|
|
1706
|
+
appName: 'tangoeye',
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
];
|
|
1710
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
// return;
|
|
1714
|
+
for ( let userData of finduserList ) {
|
|
1715
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1716
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1717
|
+
let description = `Created on ${createdOn}`;
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
let Data = {
|
|
1721
|
+
'title': title,
|
|
1722
|
+
'body': description,
|
|
1723
|
+
'type': 'review',
|
|
1724
|
+
'date': record.dateString,
|
|
1725
|
+
'storeId': record.storeId,
|
|
1726
|
+
'clientId': record.clientId,
|
|
1727
|
+
'ticketId': record.ticketId,
|
|
1728
|
+
};
|
|
1729
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1730
|
+
|
|
1731
|
+
if ( ticketsFeature ) {
|
|
1732
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1733
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1734
|
+
const fcmToken = userData.fcmToken;
|
|
1735
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1742
|
+
|
|
1743
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1744
|
+
|
|
1745
|
+
if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
|
|
1746
|
+
if ( record.status === 'Closed' ) {
|
|
1747
|
+
const query = {
|
|
1748
|
+
storeId: inputData?.storeId,
|
|
1749
|
+
isVideoStream: true,
|
|
1750
|
+
};
|
|
1751
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
1752
|
+
const revopInfoQuery = {
|
|
1753
|
+
size: 50000,
|
|
1754
|
+
query: {
|
|
1755
|
+
bool: {
|
|
1756
|
+
must: [
|
|
1757
|
+
{
|
|
1758
|
+
term: {
|
|
1759
|
+
'storeId.keyword': inputData.storeId,
|
|
1760
|
+
},
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
term: {
|
|
1764
|
+
'dateString': inputData.dateString,
|
|
1765
|
+
},
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
term: {
|
|
1769
|
+
'isParent': false,
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
term: {
|
|
1774
|
+
isChecked: true,
|
|
1775
|
+
},
|
|
1776
|
+
},
|
|
1777
|
+
],
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
_source: [ 'tempId' ],
|
|
1781
|
+
|
|
1782
|
+
};
|
|
1783
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
1784
|
+
// Get all tempIds from revopInfo response
|
|
1785
|
+
const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
1786
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
1787
|
+
const managerEyeZoneQuery = {
|
|
1788
|
+
size: 1,
|
|
1789
|
+
query: {
|
|
1790
|
+
bool: {
|
|
1791
|
+
must: [
|
|
1792
|
+
{
|
|
1793
|
+
term: {
|
|
1794
|
+
'storeId.keyword': inputData.storeId,
|
|
1795
|
+
},
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
term: {
|
|
1799
|
+
'zoneId.keyword': 'Overall Store',
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
term: {
|
|
1804
|
+
'storeDate': inputData.dateString,
|
|
1805
|
+
},
|
|
1806
|
+
},
|
|
1807
|
+
],
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
// Query the managerEyeZone index for the matching document
|
|
1814
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
1815
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
1816
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
1817
|
+
const mapping =
|
|
1818
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
1819
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
1820
|
+
{ tempId: '' };
|
|
1821
|
+
|
|
1822
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
1823
|
+
const temp = [];
|
|
1824
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) : '' );
|
|
1825
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
1826
|
+
if ( isSendMessge == true ) {
|
|
1827
|
+
logger.info( '....1' );
|
|
1828
|
+
}
|
|
1829
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1830
|
+
let Ticket = await getOpenSearchById( openSearch.footfallDirectory, id );
|
|
1831
|
+
if ( Ticket?.body?._source?.type === 'store' ) {
|
|
1832
|
+
let findTagging = Ticket?.body?._source?.mappingInfo.filter( ( data ) => data.type === 'tagging' );
|
|
1833
|
+
if ( findTagging?.length > 0 && findTagging[0].createdByEmail != '' ) {
|
|
1834
|
+
let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
|
|
1835
|
+
let title = `Received response for the Footfall ticket raised.`;
|
|
1836
|
+
let createdOn = dayjs( Ticket?.body?._source?.dateString ).format( 'DD MMM YYYY' );
|
|
1837
|
+
let description = `Raised on ${createdOn}`;
|
|
1838
|
+
|
|
1839
|
+
let Data = {
|
|
1840
|
+
'title': title,
|
|
1841
|
+
'body': description,
|
|
1842
|
+
'type': 'closed',
|
|
1843
|
+
'date': Ticket?.body?._source?.dateString,
|
|
1844
|
+
'storeId': Ticket?.body?._source?.storeId,
|
|
1845
|
+
'clientId': Ticket?.body?._source?.clientId,
|
|
1846
|
+
'ticketId': Ticket?.body?._source?.ticketId,
|
|
1847
|
+
};
|
|
1848
|
+
if ( userData && userData.fcmToken ) {
|
|
1849
|
+
const fcmToken = userData.fcmToken;
|
|
1850
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1856
|
+
} else {
|
|
1857
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1858
|
+
}
|
|
1859
|
+
} catch ( error ) {
|
|
1860
|
+
const err = error.message || 'Internal Server Error';
|
|
1861
|
+
logger.error( { error: error, funtion: 'ticketreview' } );
|
|
1862
|
+
return res.sendError( err, 500 );
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
export async function ticketApprove( req, res, next ) {
|
|
1867
|
+
try {
|
|
1868
|
+
const inputData = req.body;
|
|
1869
|
+
if ( inputData?.type !== 'approve' ) {
|
|
1870
|
+
return next();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
// check the createtion permission from the user permission
|
|
1875
|
+
const userInfo = req?.user;
|
|
1876
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1877
|
+
if ( !ticketsFeature ) {
|
|
1878
|
+
return res.sendError( 'Forbidden to Approve this Ticket', 403 );
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// get store info by the storeId into mongo db
|
|
1882
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
1883
|
+
|
|
1884
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
1885
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// get the footfall count from opensearch
|
|
1889
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1890
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
1891
|
+
const getQuery = {
|
|
1892
|
+
query: {
|
|
1893
|
+
terms: {
|
|
1894
|
+
_id: [ dateString ],
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
1898
|
+
sort: [
|
|
1899
|
+
{
|
|
1900
|
+
date_iso: {
|
|
1901
|
+
order: 'desc',
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
],
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
1908
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
1909
|
+
|
|
1910
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
1911
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// get category details from the client level configuration
|
|
1915
|
+
const configQuery = [
|
|
1916
|
+
{
|
|
1917
|
+
$match: {
|
|
1918
|
+
clientId: getstoreName?.clientId,
|
|
1919
|
+
},
|
|
1920
|
+
},
|
|
1921
|
+
|
|
1922
|
+
// Convert all effectiveFrom to proper Date
|
|
1923
|
+
{
|
|
1924
|
+
$addFields: {
|
|
1925
|
+
taggingLimitationWithDate: {
|
|
1926
|
+
$map: {
|
|
1927
|
+
input: '$footfallDirectoryConfigs.taggingLimitation',
|
|
1928
|
+
as: 'item',
|
|
1929
|
+
in: {
|
|
1930
|
+
effectiveFrom: { $toDate: '$$item.effectiveFrom' },
|
|
1931
|
+
values: '$$item.values',
|
|
1932
|
+
},
|
|
1933
|
+
},
|
|
1934
|
+
},
|
|
1935
|
+
},
|
|
1936
|
+
},
|
|
1937
|
+
|
|
1938
|
+
// Filter items <= input date
|
|
1939
|
+
{
|
|
1940
|
+
$addFields: {
|
|
1941
|
+
matchedLimitation: {
|
|
1942
|
+
$filter: {
|
|
1943
|
+
input: '$taggingLimitationWithDate',
|
|
1944
|
+
as: 'item',
|
|
1945
|
+
cond: {
|
|
1946
|
+
$lte: [
|
|
1947
|
+
'$$item.effectiveFrom',
|
|
1948
|
+
{ $toDate: inputData.dateString },
|
|
1949
|
+
],
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
},
|
|
1953
|
+
},
|
|
1954
|
+
},
|
|
1955
|
+
|
|
1956
|
+
// Sort DESC and pick ONLY top 1 -> latest effective record
|
|
1957
|
+
{
|
|
1958
|
+
$addFields: {
|
|
1959
|
+
effectiveLimitation: {
|
|
1960
|
+
$arrayElemAt: [
|
|
1961
|
+
{
|
|
1962
|
+
$slice: [
|
|
1963
|
+
{
|
|
1964
|
+
$sortArray: {
|
|
1965
|
+
input: '$matchedLimitation',
|
|
1966
|
+
sortBy: { effectiveFrom: -1 },
|
|
1967
|
+
},
|
|
1968
|
+
},
|
|
1969
|
+
1,
|
|
1970
|
+
],
|
|
1971
|
+
},
|
|
1972
|
+
0,
|
|
1973
|
+
],
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1976
|
+
},
|
|
1977
|
+
|
|
1978
|
+
{
|
|
1979
|
+
$project: {
|
|
1980
|
+
config: 1,
|
|
1981
|
+
effectiveLimitation: 1,
|
|
1982
|
+
footfallDirectoryConfigs: 1,
|
|
1983
|
+
},
|
|
1984
|
+
},
|
|
1985
|
+
];
|
|
1986
|
+
|
|
1987
|
+
|
|
1988
|
+
const config = await aggregateClient( configQuery );
|
|
1989
|
+
const getConfig = config[0];
|
|
1990
|
+
if ( !getConfig || getConfig == null ) {
|
|
1991
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
1995
|
+
const taggingLimitation = getConfig?.effectiveLimitation?.values;
|
|
1996
|
+
// Initialize count object from taggingLimitation
|
|
1997
|
+
const tempAcc = [];
|
|
1998
|
+
taggingLimitation.reduce( ( acc, item ) => {
|
|
1999
|
+
if ( item?.type ) {
|
|
2000
|
+
// Convert type to camelCase with "Count" suffix
|
|
2001
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
2002
|
+
const typeLower = item.type.toLowerCase();
|
|
2003
|
+
let key;
|
|
2004
|
+
if ( typeLower === 'housekeeping' ) {
|
|
2005
|
+
key = 'houseKeepingCount';
|
|
2006
|
+
} else {
|
|
2007
|
+
// Convert first letter to lowercase and append "Count"
|
|
2008
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
2013
|
+
tempAcc.push( {
|
|
2014
|
+
name: item.name,
|
|
2015
|
+
value: 0,
|
|
2016
|
+
key: key,
|
|
2017
|
+
type: item.type,
|
|
2018
|
+
} );
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
return acc;
|
|
2022
|
+
}
|
|
2023
|
+
}, {} ) || {};
|
|
2024
|
+
|
|
2025
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
2026
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
2027
|
+
const revopQuery = {
|
|
2028
|
+
size: 0,
|
|
2029
|
+
query: {
|
|
2030
|
+
bool: {
|
|
2031
|
+
must: [
|
|
2032
|
+
{
|
|
2033
|
+
term: {
|
|
2034
|
+
'storeId.keyword': inputData.storeId,
|
|
2035
|
+
},
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
term: {
|
|
2039
|
+
'dateString': inputData.dateString,
|
|
2040
|
+
},
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
term: {
|
|
2044
|
+
'isParent': false,
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
term: {
|
|
2049
|
+
isChecked: true,
|
|
2050
|
+
},
|
|
2051
|
+
},
|
|
2052
|
+
],
|
|
2053
|
+
},
|
|
2054
|
+
},
|
|
2055
|
+
aggs: {
|
|
2056
|
+
type_counts: {
|
|
2057
|
+
terms: {
|
|
2058
|
+
field: 'revopsType.keyword',
|
|
2059
|
+
size: 50000,
|
|
2060
|
+
},
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
2067
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
2068
|
+
|
|
2069
|
+
// Map OpenSearch revopsType values to count object keys
|
|
2070
|
+
buckets.forEach( ( bucket ) => {
|
|
2071
|
+
const revopsType = bucket.key;
|
|
2072
|
+
const count = bucket.doc_count || 0;
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
2076
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
2077
|
+
const accMatch = tempAcc.find(
|
|
2078
|
+
( acc ) =>
|
|
2079
|
+
acc.type &&
|
|
2080
|
+
acc.type === revopsType,
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
if ( accMatch && accMatch.key ) {
|
|
2084
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
} );
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
2092
|
+
|
|
2093
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
2094
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
2095
|
+
0;
|
|
2096
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
2097
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
2098
|
+
|
|
2099
|
+
// if ( footfallCount - revisedFootfall == 0 ) {
|
|
2100
|
+
// return res.sendError( 'Cannot approve a ticket because footfall hasn’t changed', 400 );
|
|
2101
|
+
// }
|
|
2102
|
+
|
|
2103
|
+
const taggingData = {
|
|
2104
|
+
size: 50000,
|
|
2105
|
+
query: {
|
|
2106
|
+
bool: {
|
|
2107
|
+
must: [
|
|
2108
|
+
{
|
|
2109
|
+
term: {
|
|
2110
|
+
'storeId.keyword': inputData.storeId,
|
|
2111
|
+
},
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
term: {
|
|
2115
|
+
'dateString': inputData.dateString,
|
|
2116
|
+
},
|
|
2117
|
+
},
|
|
2118
|
+
],
|
|
2119
|
+
},
|
|
2120
|
+
},
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
2124
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
2125
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
2126
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
2130
|
+
|
|
2131
|
+
const getTicket = {
|
|
2132
|
+
size: 50000,
|
|
2133
|
+
query: {
|
|
2134
|
+
bool: {
|
|
2135
|
+
must: [
|
|
2136
|
+
{
|
|
2137
|
+
term: {
|
|
2138
|
+
'type.keyword': 'store',
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
term: {
|
|
2143
|
+
'storeId.keyword': inputData.storeId,
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
term: {
|
|
2148
|
+
'dateString': inputData.dateString,
|
|
2149
|
+
},
|
|
2150
|
+
},
|
|
2151
|
+
],
|
|
2152
|
+
},
|
|
2153
|
+
},
|
|
2154
|
+
};
|
|
2155
|
+
|
|
2156
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
2157
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
2158
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
2159
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const approveDueDate = ticketData?.[0]?._source?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate;
|
|
2163
|
+
if ( dayjs().isAfter( dayjs( approveDueDate ), 'day' ) ) {
|
|
2164
|
+
return res.sendError( 'Ticket approve is not allowed beyond the due date', 400 );
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
const record = {
|
|
2168
|
+
|
|
2169
|
+
status: 'Closed',
|
|
2170
|
+
revicedFootfall: revisedFootfall,
|
|
2171
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2172
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2173
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
2174
|
+
// createdByEmail: req?.user?.email,
|
|
2175
|
+
// createdByUserName: req?.user?.userName,
|
|
2176
|
+
// createdByRole: req?.user?.role,
|
|
2177
|
+
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
2182
|
+
let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
|
|
2183
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
2184
|
+
|
|
2185
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
2186
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
2187
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
2188
|
+
const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
|
|
2189
|
+
const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
|
|
2190
|
+
const tangoApproved = getConfig?.footfallDirectoryConfigs?.tangoApproved || false;
|
|
2191
|
+
const tangoDueDate = getConfig?.footfallDirectoryConfigs?.allowTangoReview || 0;
|
|
2192
|
+
|
|
2193
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
2194
|
+
|
|
2195
|
+
if (
|
|
2196
|
+
isAutoCloseEnable === true &&
|
|
2197
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
2198
|
+
) {
|
|
2199
|
+
record.status = 'Closed';
|
|
2200
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
2201
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2202
|
+
const temp = record.mappingInfo
|
|
2203
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
2204
|
+
.map( ( item ) => ( {
|
|
2205
|
+
...item,
|
|
2206
|
+
|
|
2207
|
+
mode: inputData.mode,
|
|
2208
|
+
revicedFootfall: revisedFootfall,
|
|
2209
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2210
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2211
|
+
count: tempAcc,
|
|
2212
|
+
revisedDetail: formattedTaggingData,
|
|
2213
|
+
status: 'Closed',
|
|
2214
|
+
createdByEmail: req?.user?.email,
|
|
2215
|
+
createdByUserName: req?.user?.userName,
|
|
2216
|
+
createdByRole: req?.user?.role,
|
|
2217
|
+
updatedAt: new Date(),
|
|
2218
|
+
// createdAt: new Date(),
|
|
2219
|
+
} ) );
|
|
2220
|
+
|
|
2221
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
2222
|
+
...temp ];
|
|
2223
|
+
// If updating the mapping config to mark [i].status as 'Closed'
|
|
2224
|
+
// Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
|
|
2225
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2226
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
2227
|
+
return {
|
|
2228
|
+
...item,
|
|
2229
|
+
status: 'Closed',
|
|
2230
|
+
};
|
|
2231
|
+
} );
|
|
2232
|
+
}
|
|
2233
|
+
// If no review mapping existed, push a new one
|
|
2234
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
2235
|
+
record.mappingInfo.push( {
|
|
2236
|
+
type: 'approve',
|
|
2237
|
+
mode: inputData.mode,
|
|
2238
|
+
revicedFootfall: revisedFootfall,
|
|
2239
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2240
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2241
|
+
count: tempAcc,
|
|
2242
|
+
revisedDetail: formattedTaggingData,
|
|
2243
|
+
status: 'Closed',
|
|
2244
|
+
createdByEmail: req?.user?.email,
|
|
2245
|
+
createdByUserName: req?.user?.userName,
|
|
2246
|
+
createdByRole: req?.user?.role,
|
|
2247
|
+
updatedAt: new Date(),
|
|
2248
|
+
} );
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
record.mappingInfo.push(
|
|
2252
|
+
{
|
|
2253
|
+
type: 'finalRevision',
|
|
2254
|
+
mode: inputData.mode,
|
|
2255
|
+
revicedFootfall: revisedFootfall,
|
|
2256
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2257
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2258
|
+
count: tempAcc,
|
|
2259
|
+
revisedDetail: formattedTaggingData,
|
|
2260
|
+
status: 'Closed',
|
|
2261
|
+
createdByEmail: req?.user?.email,
|
|
2262
|
+
createdByUserName: req?.user?.userName,
|
|
2263
|
+
createdByRole: req?.user?.role,
|
|
2264
|
+
createdAt: new Date(),
|
|
2265
|
+
},
|
|
2266
|
+
);
|
|
2267
|
+
} else if ( revised < tangoReview ) {
|
|
2268
|
+
let approverMapping = null;
|
|
2269
|
+
let tangoReviewMapping = null;
|
|
2270
|
+
if ( tangoApproved ) {
|
|
2271
|
+
record.status = 'Approver-Closed';
|
|
2272
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
2273
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2274
|
+
const temp = record.mappingInfo
|
|
2275
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
2276
|
+
.map( ( item ) => ( {
|
|
2277
|
+
...item,
|
|
2278
|
+
mode: inputData.mode,
|
|
2279
|
+
revicedFootfall: revisedFootfall,
|
|
2280
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2281
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2282
|
+
count: tempAcc,
|
|
2283
|
+
revisedDetail: formattedTaggingData,
|
|
2284
|
+
status: 'Under Tango Review',
|
|
2285
|
+
createdByEmail: req?.user?.email,
|
|
2286
|
+
createdByUserName: req?.user?.userName,
|
|
2287
|
+
createdByRole: req?.user?.role,
|
|
2288
|
+
updatedAt: new Date(),
|
|
2289
|
+
} ) );
|
|
2290
|
+
|
|
2291
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
2292
|
+
...temp ];
|
|
2293
|
+
|
|
2294
|
+
// If no review mapping existed, push a new one
|
|
2295
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
2296
|
+
record.mappingInfo.push( {
|
|
2297
|
+
type: 'approve',
|
|
2298
|
+
mode: inputData.mode,
|
|
2299
|
+
revicedFootfall: revisedFootfall,
|
|
2300
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2301
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2302
|
+
count: tempAcc,
|
|
2303
|
+
revisedDetail: formattedTaggingData,
|
|
2304
|
+
status: 'Under Tango Review',
|
|
2305
|
+
createdByEmail: req?.user?.email,
|
|
2306
|
+
createdByUserName: req?.user?.userName,
|
|
2307
|
+
createdByRole: req?.user?.role,
|
|
2308
|
+
updatedAt: new Date(),
|
|
2309
|
+
} );
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// Find out which roles have isChecked true
|
|
2314
|
+
|
|
2315
|
+
// for ( const r of revisionArray ) {
|
|
2316
|
+
// if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
2317
|
+
|
|
2318
|
+
tangoReviewMapping = {
|
|
2319
|
+
type: 'tangoreview',
|
|
2320
|
+
count: tempAcc,
|
|
2321
|
+
revisedDetail: formattedTaggingData,
|
|
2322
|
+
status: 'Open',
|
|
2323
|
+
dueDate: new Date( Date.now() + tangoDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
2324
|
+
createdAt: new Date(),
|
|
2325
|
+
updatedAt: new Date(),
|
|
2326
|
+
};
|
|
2327
|
+
// }
|
|
2328
|
+
// }
|
|
2329
|
+
} else {
|
|
2330
|
+
record.status ='Closed';
|
|
2331
|
+
|
|
2332
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2333
|
+
const temp = record.mappingInfo
|
|
2334
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
2335
|
+
.map( ( item ) => ( {
|
|
2336
|
+
...item,
|
|
2337
|
+
mode: inputData.mode,
|
|
2338
|
+
revicedFootfall: revisedFootfall,
|
|
2339
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2340
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2341
|
+
count: tempAcc,
|
|
2342
|
+
revisedDetail: formattedTaggingData,
|
|
2343
|
+
status: 'Closed',
|
|
2344
|
+
createdByEmail: req?.user?.email,
|
|
2345
|
+
createdByUserName: req?.user?.userName,
|
|
2346
|
+
createdByRole: req?.user?.role,
|
|
2347
|
+
updatedAt: new Date(),
|
|
2348
|
+
} ) );
|
|
2349
|
+
|
|
2350
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
2351
|
+
...temp ];
|
|
2352
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2353
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
2354
|
+
return {
|
|
2355
|
+
...item,
|
|
2356
|
+
status: 'Closed',
|
|
2357
|
+
};
|
|
2358
|
+
} );
|
|
2359
|
+
}
|
|
2360
|
+
// If no review mapping existed, push a new one
|
|
2361
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
2362
|
+
record.mappingInfo.push( {
|
|
2363
|
+
type: 'approve',
|
|
2364
|
+
mode: inputData.mode,
|
|
2365
|
+
revicedFootfall: revisedFootfall,
|
|
2366
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2367
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2368
|
+
count: tempAcc,
|
|
2369
|
+
revisedDetail: formattedTaggingData,
|
|
2370
|
+
status: 'Closed',
|
|
2371
|
+
createdByEmail: req?.user?.email,
|
|
2372
|
+
createdByUserName: req?.user?.userName,
|
|
2373
|
+
createdByRole: req?.user?.role,
|
|
2374
|
+
updatedAt: new Date(),
|
|
2375
|
+
} );
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
record.mappingInfo.push(
|
|
2379
|
+
{
|
|
2380
|
+
type: 'finalRevision',
|
|
2381
|
+
mode: inputData.mode,
|
|
2382
|
+
revicedFootfall: revisedFootfall,
|
|
2383
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2384
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2385
|
+
count: tempAcc,
|
|
2386
|
+
revisedDetail: formattedTaggingData,
|
|
2387
|
+
status: 'Closed',
|
|
2388
|
+
createdByEmail: req?.user?.email,
|
|
2389
|
+
createdByUserName: req?.user?.userName,
|
|
2390
|
+
createdByRole: req?.user?.role,
|
|
2391
|
+
createdAt: new Date(),
|
|
2392
|
+
updatedAt: new Date(),
|
|
2393
|
+
},
|
|
2394
|
+
);
|
|
2395
|
+
tangoReviewMapping = {
|
|
2396
|
+
type: 'tangoreview',
|
|
2397
|
+
count: tempAcc,
|
|
2398
|
+
revisedDetail: formattedTaggingData,
|
|
2399
|
+
status: 'Open',
|
|
2400
|
+
dueDate: new Date( Date.now() + tangoDueDate * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
2401
|
+
createdAt: new Date(),
|
|
2402
|
+
updatedAt: new Date(),
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if ( approverMapping ) {
|
|
2407
|
+
// If approver and checked
|
|
2408
|
+
record.mappingInfo.push( approverMapping );
|
|
2409
|
+
} else if ( tangoReviewMapping ) {
|
|
2410
|
+
// If none above, then tangoReview
|
|
2411
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
record.status ='Closed';
|
|
2415
|
+
|
|
2416
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2417
|
+
const temp = record.mappingInfo
|
|
2418
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
2419
|
+
.map( ( item ) => ( {
|
|
2420
|
+
...item,
|
|
2421
|
+
mode: inputData.mode,
|
|
2422
|
+
revicedFootfall: revisedFootfall,
|
|
2423
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2424
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2425
|
+
count: tempAcc,
|
|
2426
|
+
revisedDetail: formattedTaggingData,
|
|
2427
|
+
status: 'Closed',
|
|
2428
|
+
createdByEmail: req?.user?.email,
|
|
2429
|
+
createdByUserName: req?.user?.userName,
|
|
2430
|
+
createdByRole: req?.user?.role,
|
|
2431
|
+
updatedAt: new Date(),
|
|
2432
|
+
} ) );
|
|
2433
|
+
|
|
2434
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
2435
|
+
...temp ];
|
|
2436
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
2437
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
2438
|
+
return {
|
|
2439
|
+
...item,
|
|
2440
|
+
status: 'Closed',
|
|
2441
|
+
};
|
|
2442
|
+
} );
|
|
2443
|
+
}
|
|
2444
|
+
// If no review mapping existed, push a new one
|
|
2445
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
2446
|
+
record.mappingInfo.push( {
|
|
2447
|
+
type: 'approve',
|
|
2448
|
+
mode: inputData.mode,
|
|
2449
|
+
revicedFootfall: revisedFootfall,
|
|
2450
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2451
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2452
|
+
count: tempAcc,
|
|
2453
|
+
revisedDetail: formattedTaggingData,
|
|
2454
|
+
status: 'Closed',
|
|
2455
|
+
createdByEmail: req?.user?.email,
|
|
2456
|
+
createdByUserName: req?.user?.userName,
|
|
2457
|
+
createdByRole: req?.user?.role,
|
|
2458
|
+
updatedAt: new Date(),
|
|
2459
|
+
} );
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
record.mappingInfo.push(
|
|
2463
|
+
{
|
|
2464
|
+
type: 'finalRevision',
|
|
2465
|
+
mode: inputData.mode,
|
|
2466
|
+
revicedFootfall: revisedFootfall,
|
|
2467
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
2468
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
2469
|
+
count: tempAcc,
|
|
2470
|
+
revisedDetail: formattedTaggingData,
|
|
2471
|
+
status: 'Closed',
|
|
2472
|
+
createdByEmail: req?.user?.email,
|
|
2473
|
+
createdByUserName: req?.user?.userName,
|
|
2474
|
+
createdByRole: req?.user?.role,
|
|
2475
|
+
createdAt: new Date(),
|
|
2476
|
+
updatedAt: new Date(),
|
|
2477
|
+
},
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
|
|
2481
|
+
|
|
2482
|
+
|
|
2483
|
+
if ( checkapprove.length > 0 ) {
|
|
2484
|
+
let userQuery = [
|
|
2485
|
+
{
|
|
2486
|
+
$match: {
|
|
2487
|
+
clientId: getstoreName.clientId,
|
|
2488
|
+
role: 'admin',
|
|
2489
|
+
isActive: true,
|
|
2490
|
+
appName: 'tangoeye',
|
|
2491
|
+
},
|
|
2492
|
+
},
|
|
2493
|
+
];
|
|
2494
|
+
let finduserList = await aggregateUser( userQuery );
|
|
2495
|
+
|
|
2496
|
+
|
|
2497
|
+
for ( let userData of finduserList ) {
|
|
2498
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
2499
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
2500
|
+
let description = `Created on ${createdOn}`;
|
|
2501
|
+
|
|
2502
|
+
let Data = {
|
|
2503
|
+
'title': title,
|
|
2504
|
+
'body': description,
|
|
2505
|
+
'type': 'approve',
|
|
2506
|
+
'date': inputData.dateString,
|
|
2507
|
+
'storeId': inputData.storeId,
|
|
2508
|
+
'clientId': record.clientId?record.clientId:'',
|
|
2509
|
+
'ticketId': record.ticketId?record.ticketId:'',
|
|
2510
|
+
};
|
|
2511
|
+
|
|
2512
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
2513
|
+
|
|
2514
|
+
if ( ticketsFeature ) {
|
|
2515
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
2516
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
2517
|
+
const fcmToken = userData.fcmToken;
|
|
2518
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
// return;
|
|
2524
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
2525
|
+
logger.info( { status: id } );
|
|
2526
|
+
|
|
2527
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
2528
|
+
|
|
2529
|
+
|
|
2530
|
+
if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
|
|
2531
|
+
if ( record.status === 'Closed' ) {
|
|
2532
|
+
const query = {
|
|
2533
|
+
storeId: inputData?.storeId,
|
|
2534
|
+
isVideoStream: true,
|
|
2535
|
+
};
|
|
2536
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
2537
|
+
const revopInfoQuery = {
|
|
2538
|
+
size: 50000,
|
|
2539
|
+
query: {
|
|
2540
|
+
bool: {
|
|
2541
|
+
must: [
|
|
2542
|
+
{
|
|
2543
|
+
term: {
|
|
2544
|
+
'storeId.keyword': inputData.storeId,
|
|
2545
|
+
},
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
term: {
|
|
2549
|
+
'dateString': inputData.dateString,
|
|
2550
|
+
},
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
term: {
|
|
2554
|
+
'isParent': false,
|
|
2555
|
+
},
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
term: {
|
|
2559
|
+
isChecked: true,
|
|
2560
|
+
},
|
|
2561
|
+
},
|
|
2562
|
+
],
|
|
2563
|
+
},
|
|
2564
|
+
},
|
|
2565
|
+
_source: [ 'tempId' ],
|
|
2566
|
+
|
|
2567
|
+
};
|
|
2568
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
2569
|
+
|
|
2570
|
+
const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
2571
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
2572
|
+
|
|
2573
|
+
const managerEyeZoneQuery = {
|
|
2574
|
+
size: 1,
|
|
2575
|
+
query: {
|
|
2576
|
+
bool: {
|
|
2577
|
+
must: [
|
|
2578
|
+
{
|
|
2579
|
+
term: {
|
|
2580
|
+
'storeId.keyword': inputData.storeId,
|
|
2581
|
+
},
|
|
2582
|
+
},
|
|
2583
|
+
{
|
|
2584
|
+
term: {
|
|
2585
|
+
'zoneId.keyword': 'Overall Store',
|
|
2586
|
+
},
|
|
2587
|
+
},
|
|
2588
|
+
{
|
|
2589
|
+
term: {
|
|
2590
|
+
'storeDate': inputData.dateString,
|
|
2591
|
+
},
|
|
2592
|
+
},
|
|
2593
|
+
],
|
|
2594
|
+
},
|
|
2595
|
+
},
|
|
2596
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// Query the managerEyeZone index for the matching document
|
|
2600
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
2601
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
2602
|
+
|
|
2603
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
2604
|
+
const mapping =
|
|
2605
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
2606
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
2607
|
+
{ tempId: '' };
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
2611
|
+
const temp = [];
|
|
2612
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) : '' );
|
|
2613
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
2614
|
+
if ( isSendMessge == true ) {
|
|
2615
|
+
logger.info( '....1' );
|
|
2616
|
+
}
|
|
2617
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
2618
|
+
logger.info( { status: id } );
|
|
2619
|
+
|
|
2620
|
+
logger.info( { status: record.status } );
|
|
2621
|
+
let Ticket = await getOpenSearchById( openSearch.footfallDirectory, id );
|
|
2622
|
+
if ( Ticket?.body?._source?.type === 'store' ) {
|
|
2623
|
+
let findTagging = Ticket?.body?._source?.mappingInfo.filter( ( data ) => data.type === 'tagging' );
|
|
2624
|
+
if ( findTagging?.length > 0 && findTagging[0].createdByEmail != '' ) {
|
|
2625
|
+
let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
|
|
2626
|
+
let title = `Received response for the Footfall ticket raised.`;
|
|
2627
|
+
let createdOn = dayjs( Ticket?.body?._source?.dateString ).format( 'DD MMM YYYY' );
|
|
2628
|
+
let description = `Raised on ${createdOn}`;
|
|
2629
|
+
|
|
2630
|
+
let Data = {
|
|
2631
|
+
'title': title,
|
|
2632
|
+
'body': description,
|
|
2633
|
+
'type': 'closed',
|
|
2634
|
+
'date': Ticket?.body?._source?.dateString,
|
|
2635
|
+
'storeId': Ticket?.body?._source?.storeId,
|
|
2636
|
+
'clientId': Ticket?.body?._source?.clientId,
|
|
2637
|
+
'ticketId': Ticket?.body?._source?.ticketId,
|
|
2638
|
+
};
|
|
2639
|
+
if ( userData && userData.fcmToken ) {
|
|
2640
|
+
const fcmToken = userData.fcmToken;
|
|
2641
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
2647
|
+
} else {
|
|
2648
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
2649
|
+
}
|
|
2650
|
+
} catch ( error ) {
|
|
2651
|
+
const err = error.message || 'Internal Server Error';
|
|
2652
|
+
logger.error( { error: error, funtion: 'ticketApprove' } );
|
|
2653
|
+
return res.sendError( err, 500 );
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
export async function getAssinedStore( user, storeId ) {
|
|
2658
|
+
if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const clientId = user.clientId;
|
|
2663
|
+
const storeIds = new Set(
|
|
2664
|
+
user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
|
|
2665
|
+
);
|
|
2666
|
+
|
|
2667
|
+
const addClusterStores = ( clusters ) => {
|
|
2668
|
+
if ( !clusters?.length ) return;
|
|
2669
|
+
for ( const cluster of clusters ) {
|
|
2670
|
+
cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
// Fetch all top-level data in parallel
|
|
2675
|
+
const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
|
|
2676
|
+
findcluster( {
|
|
2677
|
+
clientId,
|
|
2678
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
2679
|
+
} ),
|
|
2680
|
+
findteams( {
|
|
2681
|
+
clientId,
|
|
2682
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
2683
|
+
} ),
|
|
2684
|
+
findteams( {
|
|
2685
|
+
clientId,
|
|
2686
|
+
users: { $elemMatch: { email: user.email } },
|
|
2687
|
+
} ),
|
|
2688
|
+
] );
|
|
2689
|
+
|
|
2690
|
+
// 1) Clusters where this user is Teamlead
|
|
2691
|
+
addClusterStores( clustersList );
|
|
2692
|
+
|
|
2693
|
+
// 2) Teams where this user is Teamlead → their users + their clusters
|
|
2694
|
+
if ( teamsList?.length ) {
|
|
2695
|
+
for ( const team of teamsList ) {
|
|
2696
|
+
if ( !team.users?.length ) continue;
|
|
2697
|
+
|
|
2698
|
+
await Promise.all(
|
|
2699
|
+
team.users.map( async ( teamUser ) => {
|
|
2700
|
+
const foundUser = await findOneUser( { _id: teamUser.userId } );
|
|
2701
|
+
if ( !foundUser ) return;
|
|
2702
|
+
|
|
2703
|
+
// Direct assigned stores of that user
|
|
2704
|
+
if ( foundUser.assignedStores?.length ) {
|
|
2705
|
+
foundUser.assignedStores.forEach( ( store ) =>
|
|
2706
|
+
storeIds.add( store.storeId ),
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// Clusters where this user is Teamlead
|
|
2711
|
+
const userClustersList = await findcluster( {
|
|
2712
|
+
clientId,
|
|
2713
|
+
Teamlead: { $elemMatch: { email: foundUser.email } },
|
|
2714
|
+
} );
|
|
2715
|
+
addClusterStores( userClustersList );
|
|
2716
|
+
} ),
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// 3) Teams where this user is a member → clusters by teamName
|
|
2722
|
+
if ( teamMemberList?.length ) {
|
|
2723
|
+
for ( const team of teamMemberList ) {
|
|
2724
|
+
const clusterList = await findcluster( {
|
|
2725
|
+
clientId,
|
|
2726
|
+
teams: { $elemMatch: { name: team.teamName } },
|
|
2727
|
+
} );
|
|
2728
|
+
addClusterStores( clusterList );
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const assignedStores = Array.from( storeIds );
|
|
2733
|
+
|
|
2734
|
+
// Previously you returned `true` in both branches.
|
|
2735
|
+
// Assuming you actually want to check membership:
|
|
2736
|
+
return assignedStores.includes( storeId );
|
|
2737
|
+
}
|
|
2738
|
+
|