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