tango-app-api-analysis-traffic 3.8.7-vms.0 → 3.8.7-vms.10
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-analysis-traffic",
|
|
3
|
-
"version": "3.8.7-vms.
|
|
3
|
+
"version": "3.8.7-vms.10",
|
|
4
4
|
"description": "Traffic Analysis",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"mongodb": "^6.8.0",
|
|
24
24
|
"nodemon": "^3.1.4",
|
|
25
25
|
"swagger-ui-express": "^5.0.1",
|
|
26
|
-
"tango-api-schema": "^2.4.
|
|
26
|
+
"tango-api-schema": "^2.4.29",
|
|
27
27
|
"tango-app-api-middleware": "^3.6.5",
|
|
28
28
|
"winston": "^3.13.1",
|
|
29
29
|
"winston-daily-rotate-file": "^5.0.0"
|
|
@@ -2,7 +2,8 @@ import { logger, insertOpenSearchData, getOpenSearchData, updateOpenSearchData }
|
|
|
2
2
|
import { findOnerevopConfig } from '../services/revopConfig.service.js';
|
|
3
3
|
import * as clientService from '../services/clients.services.js';
|
|
4
4
|
import { bulkUpdate, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
|
|
5
|
-
import
|
|
5
|
+
import { findOneVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
|
|
6
|
+
// import dayjs from 'dayjs';
|
|
6
7
|
// Lamda Service Call //
|
|
7
8
|
async function LamdaServiceCall( url, data ) {
|
|
8
9
|
try {
|
|
@@ -211,44 +212,171 @@ export async function storeProcessedData( req, res ) {
|
|
|
211
212
|
try {
|
|
212
213
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
213
214
|
const inputData = req.query;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
215
|
+
const { fromDate, toDate, storeId } = inputData;
|
|
216
|
+
|
|
217
|
+
// Multi-date range handling for a single store
|
|
218
|
+
if ( fromDate && toDate && storeId ) {
|
|
219
|
+
const dayjs = ( await import( 'dayjs' ) ).default;
|
|
220
|
+
const isSameOrBefore = ( await import( 'dayjs/plugin/isSameOrBefore.js' ) ).default;
|
|
221
|
+
dayjs.extend( isSameOrBefore );
|
|
222
|
+
|
|
223
|
+
let start = dayjs( fromDate );
|
|
224
|
+
// get start value from the before day one
|
|
225
|
+
// Move start one day back so we can access "day before" when looping
|
|
226
|
+
start = start.subtract( 1, 'day' );
|
|
227
|
+
let end = dayjs( toDate );
|
|
228
|
+
|
|
229
|
+
if ( !start.isValid() || !end.isValid() ) {
|
|
230
|
+
return res.sendError( 'Invalid date range supplied', 400 );
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if ( end.isBefore( start ) ) {
|
|
234
|
+
[ start, end ] = [ end, start ];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const allDateStrings = [];
|
|
238
|
+
const orderedDates = [];
|
|
239
|
+
|
|
240
|
+
while ( start.isSameOrBefore( end ) ) {
|
|
241
|
+
const formatted = start.format( 'YYYY-MM-DD' );
|
|
242
|
+
orderedDates.push( formatted );
|
|
243
|
+
allDateStrings.push( `${storeId}_${formatted}` );
|
|
244
|
+
start = start.add( 1, 'day' );
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if ( allDateStrings.length === 0 ) {
|
|
248
|
+
return res.sendSuccess( [] );
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const footfallQuery = {
|
|
252
|
+
query: {
|
|
253
|
+
terms: {
|
|
254
|
+
_id: allDateStrings,
|
|
228
255
|
},
|
|
229
256
|
},
|
|
230
|
-
|
|
231
|
-
|
|
257
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
258
|
+
sort: [
|
|
259
|
+
{ date_iso: { order: 'asc' } },
|
|
260
|
+
],
|
|
261
|
+
size: allDateStrings.length,
|
|
262
|
+
};
|
|
232
263
|
|
|
233
|
-
|
|
234
|
-
|
|
264
|
+
const multiGet = await getOpenSearchData( openSearch.footfall, footfallQuery );
|
|
265
|
+
const multiHits = multiGet?.body?.hits?.hits || [];
|
|
266
|
+
const hitsMap = new Map();
|
|
267
|
+
multiHits.forEach( ( hit ) => {
|
|
268
|
+
hitsMap.set( hit?._id, hit?._source || null );
|
|
269
|
+
} );
|
|
235
270
|
|
|
236
|
-
|
|
237
|
-
const previousData = hits.find( ( d ) => d._id === dateStringPrevious )?._source || null;
|
|
271
|
+
const responseArray = [];
|
|
238
272
|
|
|
239
|
-
|
|
273
|
+
for ( let i = 1; i < orderedDates.length; i++ ) {
|
|
274
|
+
const currentDate = orderedDates[i];
|
|
275
|
+
const currentId = `${storeId}_${currentDate}`;
|
|
276
|
+
logger.info( { currentId, currentDate } );
|
|
277
|
+
const processedData = hitsMap.get( currentId );
|
|
278
|
+
logger.info( { processedData } );
|
|
279
|
+
if ( !processedData ) {
|
|
280
|
+
responseArray.push( {
|
|
281
|
+
date: currentDate,
|
|
282
|
+
footfallCount: 0,
|
|
283
|
+
footfallCountTrend: 0,
|
|
284
|
+
downtime: 0,
|
|
285
|
+
} );
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
240
288
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
289
|
+
const prevDate = dayjs( currentDate ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
|
|
290
|
+
const prevId = `${storeId}_${prevDate}`;
|
|
291
|
+
logger.info( { prevId, prevDate } );
|
|
292
|
+
const previousData = hitsMap.get( prevId );
|
|
293
|
+
|
|
294
|
+
let footfallCountTrend = 0;
|
|
295
|
+
logger.info( { previousData, previoucubr1: previousData?.footfall_count } );
|
|
296
|
+
if ( previousData && previousData.footfall_count ) {
|
|
297
|
+
logger.info( { previousData, previoucubr: previousData?.footfall_count } );
|
|
298
|
+
footfallCountTrend = Math.round(
|
|
299
|
+
( ( processedData.footfall_count - previousData?.footfall_count ) / previousData.footfall_count ) * 100,
|
|
300
|
+
);
|
|
301
|
+
logger.info( { footfallCountTrend } );
|
|
302
|
+
}
|
|
303
|
+
// Add ticket status from openSearch.footfallDirectory (_source.status)
|
|
304
|
+
let ticketStatus = null;
|
|
305
|
+
// Try to find a matching footfallDirectory record for this date+storeId
|
|
306
|
+
// const ticketKey = `${storeId}_${currentDate}`;
|
|
307
|
+
const footfallDirQuery = {
|
|
308
|
+
query: {
|
|
309
|
+
bool: {
|
|
310
|
+
must: [
|
|
311
|
+
{ term: { 'storeId.keyword': storeId } },
|
|
312
|
+
{ term: { 'dateString': currentDate } },
|
|
313
|
+
{ term: { 'ticketName.keyword': 'footfall-directory' } },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
size: 1,
|
|
318
|
+
_source: [ 'status', 'mappingInfo' ],
|
|
319
|
+
};
|
|
320
|
+
try {
|
|
321
|
+
const footfallDirRes = await getOpenSearchData( openSearch.footfallDirectory, footfallDirQuery );
|
|
322
|
+
const hit = footfallDirRes?.body?.hits?.hits?.[0];
|
|
323
|
+
if ( hit?._source?.mappingInfo && Array.isArray( hit._source.mappingInfo ) ) {
|
|
324
|
+
for ( let i = 0; i < hit._source.mappingInfo.length; i++ ) {
|
|
325
|
+
if ( hit._source.mappingInfo[i].type === 'tagging' ) {
|
|
326
|
+
ticketStatus = hit._source.mappingInfo[i].status;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch ( err ) {
|
|
332
|
+
logger.warn( { message: 'Could not get ticket status from footfallDirectory', error: err } );
|
|
333
|
+
}
|
|
334
|
+
// Check if request status ("raised") should be enabled or disabled by querying MongoDB config
|
|
335
|
+
// We'll assume findOnerevopConfig can fetch the record with status for this storeId and dateString
|
|
336
|
+
let raisedStatusEnabled = 'reset'; // default: enabled
|
|
337
|
+
try {
|
|
338
|
+
const mongoConfig = await findOneVmsStoreRequest( { storeId: storeId, dateString: currentDate } );
|
|
339
|
+
if ( mongoConfig && mongoConfig.status ) {
|
|
340
|
+
raisedStatusEnabled = mongoConfig.status;
|
|
341
|
+
}
|
|
342
|
+
} catch ( err ) {
|
|
343
|
+
logger.warn( { message: 'Could not get request status from MongoDB', error: err } );
|
|
344
|
+
// Leave raisedStatusEnabled as default
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
responseArray.push( {
|
|
348
|
+
date: processedData?.date_string || currentDate,
|
|
349
|
+
footfallCount: processedData?.footfall_count || 0,
|
|
350
|
+
footfallCountTrend,
|
|
351
|
+
downtime: processedData?.down_time || 0,
|
|
352
|
+
ticketStatus,
|
|
353
|
+
raisedStatusEnabled,
|
|
354
|
+
|
|
355
|
+
} );
|
|
356
|
+
|
|
357
|
+
if ( raisedStatusEnabled === 'block' ) {
|
|
358
|
+
// Calculate the number of days from currentDate + 1 to the end of the month
|
|
359
|
+
// Assume currentDate is in format 'YYYY-MM-DD'
|
|
360
|
+
const currentDateObj = new Date( currentDate );
|
|
361
|
+
// Move to next day
|
|
362
|
+
const nextDay = new Date( currentDateObj );
|
|
363
|
+
nextDay.setDate( currentDateObj.getDate() + 1 );
|
|
364
|
+
|
|
365
|
+
// Get the last date of the current month
|
|
366
|
+
const endOfMonth = new Date( currentDateObj.getFullYear(), currentDateObj.getMonth() + 1, 0 );
|
|
367
|
+
|
|
368
|
+
// Calculate number of days (inclusive of end date, exclusive of nextDay)
|
|
369
|
+
let noOfBlockedDays = Math.floor( ( endOfMonth - nextDay ) / ( 1000 * 60 * 60 * 24 ) ) + 1;
|
|
370
|
+
if ( noOfBlockedDays < 0 ) noOfBlockedDays = 0;
|
|
371
|
+
|
|
372
|
+
// Add to response
|
|
373
|
+
responseArray[responseArray.length - 1].noOfBlockedDays = noOfBlockedDays;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
246
376
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
downtime: processedData?.down_time || 0,
|
|
251
|
-
} );
|
|
377
|
+
return res.sendSuccess( responseArray );
|
|
378
|
+
}
|
|
379
|
+
return res.sendError( 'Required parameters missing', 400 );
|
|
252
380
|
} catch ( error ) {
|
|
253
381
|
logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
|
|
254
382
|
const err = error.message || 'Internal Server Error';
|
|
@@ -287,40 +415,177 @@ export async function footFallImages( req, res ) {
|
|
|
287
415
|
],
|
|
288
416
|
},
|
|
289
417
|
},
|
|
290
|
-
'_source': [ 'dateString', 'storeId', '
|
|
418
|
+
'_source': [ 'dateString', 'storeId', 'mappingInfo', 'revicedFootfall', 'revicedPerc', 'createdAt', 'updatedAt', 'footfallCount' ],
|
|
291
419
|
|
|
292
420
|
};
|
|
293
421
|
|
|
294
422
|
const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
|
|
295
423
|
const ticketDetails = getData?.body?.hits?.hits[0];
|
|
296
424
|
let temp = [];
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
425
|
+
const footfallValue = ticketDetails?._source?.footfallCount ?? 0;
|
|
426
|
+
const mappingInfoArray = ticketDetails?._source?.mappingInfo ?? [];
|
|
427
|
+
// Helper to get mappingInfo for an actionType
|
|
428
|
+
function getMappingForType( type ) {
|
|
429
|
+
return mappingInfoArray.find( ( m ) => m.type === type ) || {};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// List of actionTypes to process in sequence
|
|
433
|
+
|
|
434
|
+
if ( req.user.userType !== 'tango' && req.user.role !== 'superadmin' ) {
|
|
435
|
+
switch ( req.user.role ) {
|
|
436
|
+
case 'user':
|
|
437
|
+
const actionTypesUser = [ 'tagging', 'finalreview' ];
|
|
438
|
+
|
|
439
|
+
temp = [];
|
|
440
|
+
actionTypesUser.forEach( ( type ) => {
|
|
441
|
+
const mapping = getMappingForType( type );
|
|
442
|
+
if ( type === 'tagging' ) {
|
|
443
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
444
|
+
const revisedPerc =
|
|
445
|
+
footfallValue > 0 ?
|
|
446
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
447
|
+
'0';
|
|
448
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
449
|
+
temp.push( {
|
|
450
|
+
actionType: type,
|
|
451
|
+
footfall: footfallValue,
|
|
452
|
+
revicedFootfall: revisedFootfall,
|
|
453
|
+
revicedPerc: revisedPerc,
|
|
454
|
+
count: countObj,
|
|
455
|
+
createdAt: mapping.createdAt ?? '',
|
|
456
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
457
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
458
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
459
|
+
isUp: false,
|
|
460
|
+
} );
|
|
461
|
+
} else if ( type !== 'tagging' && mapping.status === 'closed' ) {
|
|
462
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
463
|
+
const revisedPerc =
|
|
464
|
+
footfallValue > 0 ?
|
|
465
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
466
|
+
'0';
|
|
467
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
468
|
+
temp.push( {
|
|
469
|
+
actionType: type,
|
|
470
|
+
footfall: footfallValue,
|
|
471
|
+
revicedFootfall: revisedFootfall,
|
|
472
|
+
revicedPerc: revisedPerc,
|
|
473
|
+
count: countObj,
|
|
474
|
+
createdAt: mapping.createdAt ?? '',
|
|
475
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
476
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
477
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
478
|
+
} );
|
|
479
|
+
}
|
|
480
|
+
} );
|
|
481
|
+
break;
|
|
482
|
+
case 'admin':
|
|
483
|
+
const actionTypesAdmin = [ 'tagging', 'review', 'finalreview' ];
|
|
484
|
+
temp = [];
|
|
485
|
+
actionTypesAdmin.forEach( ( type ) => {
|
|
486
|
+
const mapping = getMappingForType( type );
|
|
487
|
+
if ( type === 'tagging' ) {
|
|
488
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
489
|
+
const revisedPerc =
|
|
490
|
+
footfallValue > 0 ?
|
|
491
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
492
|
+
'0';
|
|
493
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
494
|
+
temp.push( {
|
|
495
|
+
actionType: type,
|
|
496
|
+
footfall: footfallValue,
|
|
497
|
+
revicedFootfall: revisedFootfall,
|
|
498
|
+
revicedPerc: revisedPerc,
|
|
499
|
+
count: countObj,
|
|
500
|
+
createdAt: mapping.createdAt ?? '',
|
|
501
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
502
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
503
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
504
|
+
isUp: false,
|
|
505
|
+
} );
|
|
506
|
+
} else if ( type !== 'tagging' && mapping.status === 'closed' ) {
|
|
507
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
508
|
+
const revisedPerc =
|
|
509
|
+
footfallValue > 0 ?
|
|
510
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
511
|
+
'0';
|
|
512
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
513
|
+
temp.push( {
|
|
514
|
+
actionType: type,
|
|
515
|
+
footfall: footfallValue,
|
|
516
|
+
revicedFootfall: revisedFootfall,
|
|
517
|
+
revicedPerc: revisedPerc,
|
|
518
|
+
count: countObj,
|
|
519
|
+
createdAt: mapping.createdAt ?? '',
|
|
520
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
521
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
522
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
523
|
+
} );
|
|
524
|
+
}
|
|
525
|
+
} );
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
const actionTypes = [ 'tagging', 'review', 'approve', 'finalreview' ];
|
|
529
|
+
|
|
530
|
+
// Dynamically add to temp only if actionType matches and status is 'closed',
|
|
531
|
+
// except for 'tagging' where status must be 'raised'
|
|
532
|
+
temp = [];
|
|
533
|
+
actionTypes.forEach( ( type ) => {
|
|
534
|
+
const mapping = getMappingForType( type );
|
|
535
|
+
if ( type === 'tagging' ) {
|
|
536
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
537
|
+
const revisedPerc =
|
|
538
|
+
footfallValue > 0 ?
|
|
539
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
540
|
+
'0';
|
|
541
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
542
|
+
temp.push( {
|
|
543
|
+
actionType: type,
|
|
544
|
+
footfall: footfallValue,
|
|
545
|
+
revicedFootfall: revisedFootfall,
|
|
546
|
+
revicedPerc: revisedPerc,
|
|
547
|
+
count: countObj,
|
|
548
|
+
createdAt: mapping.createdAt ?? '',
|
|
549
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
550
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
551
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
552
|
+
isUp: false,
|
|
553
|
+
} );
|
|
554
|
+
} else if ( type !== 'tagging' && mapping.status === 'closed' ) {
|
|
555
|
+
const revisedFootfall = mapping.revicedFootfall ?? 0;
|
|
556
|
+
const revisedPerc =
|
|
557
|
+
footfallValue > 0 ?
|
|
558
|
+
`${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
|
|
559
|
+
'0';
|
|
560
|
+
const countObj = mapping.count ? [ ...mapping.count ] : [];
|
|
561
|
+
temp.push( {
|
|
562
|
+
actionType: type,
|
|
563
|
+
footfall: footfallValue,
|
|
564
|
+
revicedFootfall: revisedFootfall,
|
|
565
|
+
revicedPerc: revisedPerc,
|
|
566
|
+
count: countObj,
|
|
567
|
+
createdAt: mapping.createdAt ?? '',
|
|
568
|
+
createdByEmail: mapping.createdByEmail ?? '',
|
|
569
|
+
createdByUserName: mapping.createdByUserName ?? '',
|
|
570
|
+
createdByRole: mapping.createdByRole ?? '',
|
|
571
|
+
} );
|
|
572
|
+
}
|
|
573
|
+
} );
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
const LamdaURL = revop.getImages;
|
|
578
|
+
let resultData = await LamdaServiceCall( LamdaURL, inputData );
|
|
579
|
+
if ( resultData ) {
|
|
580
|
+
// temp.length? temp[0].status = 'open': null;
|
|
581
|
+
if ( resultData.status_code == '200' ) {
|
|
582
|
+
return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0 && ticketDetails? temp : null, config: req?.store?.footfallDirectoryConfigs } );
|
|
583
|
+
} else {
|
|
584
|
+
return res.sendError( 'No Content', 204 );
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
return res.sendError( 'No Content', 204 );
|
|
588
|
+
}
|
|
324
589
|
} catch ( error ) {
|
|
325
590
|
logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
|
|
326
591
|
const err = error.message || 'Internal Server Error';
|
|
@@ -348,6 +613,13 @@ export async function tagTempId( req, res ) {
|
|
|
348
613
|
description: '',
|
|
349
614
|
isChecked: inputData.isChecked,
|
|
350
615
|
duplicateImage: inputData?.duplicateImage?.length>0? inputData?.duplicateImage :[],
|
|
616
|
+
// Add id to each object in duplicateImage if it exists and is an array
|
|
617
|
+
duplicateImage: Array.isArray( inputData?.duplicateImage ) ?
|
|
618
|
+
inputData.duplicateImage.map( ( img ) => ( {
|
|
619
|
+
...img,
|
|
620
|
+
id: `${inputData?.storeId}_${inputData?.dateString}_${img?.tempId}`,
|
|
621
|
+
} ) ) :
|
|
622
|
+
[],
|
|
351
623
|
type: 'tagging-reflect',
|
|
352
624
|
ticketStatus: 'submitted',
|
|
353
625
|
isParent: inputData?.revopsType === 'duplicate'? true : false,
|
|
@@ -357,6 +629,7 @@ export async function tagTempId( req, res ) {
|
|
|
357
629
|
action: 'submitted',
|
|
358
630
|
},
|
|
359
631
|
],
|
|
632
|
+
comments: inputData.comments || '',
|
|
360
633
|
createdAt: new Date(),
|
|
361
634
|
updatedAt: new Date(),
|
|
362
635
|
|
|
@@ -410,6 +683,7 @@ export async function tagTempId( req, res ) {
|
|
|
410
683
|
isParent: false,
|
|
411
684
|
type: 'tagging-reflect',
|
|
412
685
|
ticketStatus: 'submitted',
|
|
686
|
+
comments: inputData.comments || '',
|
|
413
687
|
actions: [
|
|
414
688
|
{
|
|
415
689
|
actionType: 'tagging',
|
package/src/dtos/revop.dtos.js
CHANGED
|
@@ -4,7 +4,8 @@ import dayjs from 'dayjs';
|
|
|
4
4
|
export const storeProcessedDataSchema = joi.object( {
|
|
5
5
|
|
|
6
6
|
storeId: joi.string().required(),
|
|
7
|
-
|
|
7
|
+
fromDate: joi.string().required(),
|
|
8
|
+
toDate: joi.string().required(),
|
|
8
9
|
|
|
9
10
|
} );
|
|
10
11
|
|
|
@@ -65,6 +66,9 @@ export const tagTempIdSchema = joi.object( {
|
|
|
65
66
|
revopsType: joi.string().required(),
|
|
66
67
|
timeRange: joi.string().required(),
|
|
67
68
|
isChecked: joi.boolean().required().allow( null ),
|
|
69
|
+
mode: joi.string().valid( 'web', 'mobile' ).required().messages( {
|
|
70
|
+
'any.only': 'type must be one of [mobile,web]',
|
|
71
|
+
} ),
|
|
68
72
|
duplicateImage: joi.array().items( joi.object(
|
|
69
73
|
{
|
|
70
74
|
tempId: joi.number().required(),
|
|
@@ -80,6 +84,7 @@ export const tagTempIdSchema = joi.object( {
|
|
|
80
84
|
entryTime: joi.string().required(),
|
|
81
85
|
exitTime: joi.string().required(),
|
|
82
86
|
filePath: joi.string().required(),
|
|
87
|
+
comments: joi.string().optional().allow( '' ),
|
|
83
88
|
|
|
84
89
|
} );
|
|
85
90
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { deleteByQuery, getOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
|
|
3
|
+
import { findOne } from '../services/clients.services.js';
|
|
4
4
|
|
|
5
5
|
export async function getTaggingConfig( req, res, next ) {
|
|
6
6
|
try {
|
|
7
7
|
const inputData= req.query;
|
|
8
|
-
const
|
|
8
|
+
const clientId = inputData.storeId.split( '-' )[0];
|
|
9
|
+
const getData = await findOne( { clientId: clientId }, { footfallDirectoryConfigs: 1 } );
|
|
10
|
+
|
|
11
|
+
// Convert "taggingLimitation" array (if present) to "config" object with expected key-value pairs
|
|
12
|
+
let config = {};
|
|
13
|
+
if ( getData && Array.isArray( getData.taggingLimitation ) ) {
|
|
14
|
+
for ( const item of getData.taggingLimitation ) {
|
|
15
|
+
if ( item && item.type && typeof item.value !== 'undefined' && item.unit ) {
|
|
16
|
+
config[item.type] = `${item.value}${item.unit}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
getData.config = config;
|
|
21
|
+
|
|
9
22
|
req.store = getData;
|
|
10
23
|
next();
|
|
11
24
|
} catch ( error ) {
|
|
@@ -51,41 +64,54 @@ export async function getFootfallCount( req, res, next ) {
|
|
|
51
64
|
export async function mappingConfig( req, res, next ) {
|
|
52
65
|
try {
|
|
53
66
|
const inputData = req.body;
|
|
67
|
+
|
|
54
68
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
term: {
|
|
63
|
-
'storeId.keyword': inputData.storeId,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
term: {
|
|
68
|
-
'dateString': inputData.dateString,
|
|
69
|
-
},
|
|
69
|
+
const footfallQuery ={
|
|
70
|
+
query: {
|
|
71
|
+
bool: {
|
|
72
|
+
must: [
|
|
73
|
+
{
|
|
74
|
+
term: {
|
|
75
|
+
'store_id.keyword': inputData.storeId,
|
|
70
76
|
},
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
term: {
|
|
80
|
+
'date_string': inputData.dateString,
|
|
75
81
|
},
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
],
|
|
78
85
|
},
|
|
79
|
-
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
|
|
90
|
+
const footfallOutput = await getOpenSearchData( openSearch.footfall, footfallQuery );
|
|
91
|
+
if ( footfallOutput?.body?.hits?.hits?.length === 0 ) {
|
|
92
|
+
return res.sendError( 'No updated footfall for this date', 400 );
|
|
93
|
+
}
|
|
94
|
+
const getFootfallCount = footfallOutput?.body?.hits?.hits;
|
|
95
|
+
const footfall = getFootfallCount?.[0]?._source?.footfall_count;
|
|
96
|
+
|
|
97
|
+
const getConfig = await findOne( { clientId: inputData?.storeId?.split( '-' )[0] }, { footfallDirectoryConfigs: 1 } );
|
|
98
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
99
|
+
// Find the tagging limitation for the given revopsType
|
|
100
|
+
let matchedLimitation = null;
|
|
101
|
+
if ( Array.isArray( taggingLimitation ) ) {
|
|
102
|
+
matchedLimitation = taggingLimitation.find(
|
|
103
|
+
( l ) => l.type === inputData.revopsType,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ( matchedLimitation ) {
|
|
108
|
+
// Determine the limit value
|
|
109
|
+
let limitValue = Number( matchedLimitation.value ) || 0;
|
|
110
|
+
let unit = matchedLimitation.unit;
|
|
111
|
+
|
|
112
|
+
// Assuming getData and/or getOpenSearchCount provides the actual tagged count for revopsType
|
|
113
|
+
// Query OpenSearch for current tagged count for this revopsType
|
|
114
|
+
const taggedCountQuery = {
|
|
89
115
|
query: {
|
|
90
116
|
bool: {
|
|
91
117
|
must: [
|
|
@@ -104,92 +130,51 @@ export async function mappingConfig( req, res, next ) {
|
|
|
104
130
|
'revopsType.keyword': inputData.revopsType,
|
|
105
131
|
},
|
|
106
132
|
},
|
|
133
|
+
{
|
|
134
|
+
term: {
|
|
135
|
+
'isParent': false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
107
138
|
],
|
|
108
139
|
},
|
|
109
140
|
},
|
|
110
141
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
const taggedData = await getOpenSearchCount( openSearch.revop, taggedCountQuery );
|
|
143
|
+
const taggedValue = ( taggedData?.body?.count || 0 )+ ( inputData.revopsType == 'duplicate'? inputData?.duplicateImage?.length : 1 );
|
|
144
|
+
// If the unit is %, compare percentage of taggedValue/footfall, otherwise compare taggedValue to limitValue directly
|
|
145
|
+
let isLimitExceeded = false;
|
|
146
|
+
if ( unit === '%' ) {
|
|
147
|
+
// footfall may be undefined, treat as 0 (avoid division by zero)
|
|
148
|
+
const totalFootfall = Number( footfall ) || 0;
|
|
149
|
+
const taggedPercent = totalFootfall > 0 ? ( taggedValue / totalFootfall ) * 100 : 0;
|
|
150
|
+
isLimitExceeded = taggedPercent > limitValue;
|
|
151
|
+
logger.info( {
|
|
152
|
+
limitType: 'PERCENT',
|
|
153
|
+
taggedValue,
|
|
154
|
+
totalFootfall,
|
|
155
|
+
taggedPercent,
|
|
156
|
+
limitValue,
|
|
157
|
+
isLimitExceeded,
|
|
158
|
+
forRevopsType: inputData.revopsType,
|
|
159
|
+
} );
|
|
114
160
|
} else {
|
|
115
|
-
|
|
161
|
+
// Non-percent, treat limitValue as an absolute number
|
|
162
|
+
isLimitExceeded = taggedValue > limitValue;
|
|
163
|
+
logger.info( {
|
|
164
|
+
limitType: 'ABSOLUTE',
|
|
165
|
+
taggedValue,
|
|
166
|
+
limitValue,
|
|
167
|
+
isLimitExceeded,
|
|
168
|
+
forRevopsType: inputData.revopsType,
|
|
169
|
+
} );
|
|
116
170
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
next();
|
|
120
|
-
return;
|
|
121
|
-
} else {
|
|
122
|
-
return res.sendError( 'Forbidden to junk mapping', 500 );
|
|
171
|
+
if ( isLimitExceeded ) {
|
|
172
|
+
return res.sendError( `Limit exceed: Only ${limitValue}${unit || ''} items allowed for ${inputData.revopsType}`, 400 );
|
|
123
173
|
}
|
|
174
|
+
return next();
|
|
124
175
|
} else {
|
|
125
|
-
next();
|
|
176
|
+
return next();
|
|
126
177
|
}
|
|
127
|
-
// else if ( inputData.revopsType == 'duplicate' ) {
|
|
128
|
-
// const getFootfallQuery = {
|
|
129
|
-
// query: {
|
|
130
|
-
// terms: {
|
|
131
|
-
// _id: [ inputData?.dateString ],
|
|
132
|
-
// },
|
|
133
|
-
// },
|
|
134
|
-
// _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
135
|
-
// sort: [
|
|
136
|
-
// {
|
|
137
|
-
// date_iso: {
|
|
138
|
-
// order: 'desc',
|
|
139
|
-
// },
|
|
140
|
-
// },
|
|
141
|
-
// ],
|
|
142
|
-
// };
|
|
143
|
-
|
|
144
|
-
// const getFootfall = await getOpenSearchData( openSearch.footfall, getFootfallQuery );
|
|
145
|
-
// const footfall = getFootfall?.body?.hites?.hits?.[0]?._source?.footfall_count;
|
|
146
|
-
// const getQuery = {
|
|
147
|
-
// query: {
|
|
148
|
-
// bool: {
|
|
149
|
-
// must: [
|
|
150
|
-
// {
|
|
151
|
-
// term: {
|
|
152
|
-
// 'storeId.keyword': inputData.storeId,
|
|
153
|
-
// },
|
|
154
|
-
// },
|
|
155
|
-
// {
|
|
156
|
-
// term: {
|
|
157
|
-
// 'dateString': inputData.dateString,
|
|
158
|
-
// },
|
|
159
|
-
// },
|
|
160
|
-
// {
|
|
161
|
-
// term: {
|
|
162
|
-
// 'revopsType.keyword': inputData.revopsType,
|
|
163
|
-
// },
|
|
164
|
-
// },
|
|
165
|
-
// {
|
|
166
|
-
// term: {
|
|
167
|
-
// 'parent.keyword': null,
|
|
168
|
-
// },
|
|
169
|
-
// },
|
|
170
|
-
// ],
|
|
171
|
-
// },
|
|
172
|
-
// },
|
|
173
|
-
// };
|
|
174
|
-
// const getData = await getOpenSearchCount( openSearch.revop, getQuery );
|
|
175
|
-
// logger.info( { getData: getData, footfall: footfall, duplicate: config?.revopTagging?.duplicate } );
|
|
176
|
-
// if ( getData && footfall && config?.revopTagging?.duplicate ) {
|
|
177
|
-
// const data = config?.revopTagging?.duplicate;
|
|
178
|
-
// // Convert "20%" → 0.2 (handle both "20%" and 20)
|
|
179
|
-
// const percentStr = typeof data === 'string' ? data.replace( '%', '' ) : data;
|
|
180
|
-
// logger.info( { percentStr: percentStr } );
|
|
181
|
-
// const percentValue =percentStr / 100;
|
|
182
|
-
|
|
183
|
-
// const result = percentValue * footfall;
|
|
184
|
-
|
|
185
|
-
// logger.info( { result: result, footfall: footfall } );
|
|
186
|
-
// }
|
|
187
|
-
// if ( getData && getData?.body?.count >= Math.round( result ) ) {
|
|
188
|
-
// return res.sendError( `Select up to ${config?.revopTagging?.duplicate} items only`, 400 );
|
|
189
|
-
// } else {
|
|
190
|
-
// next();
|
|
191
|
-
// }
|
|
192
|
-
// }
|
|
193
178
|
} catch ( error ) {
|
|
194
179
|
logger.error( { error: error, message: req.body, function: 'traffic-revop-getTaggingConfig' } );
|
|
195
180
|
next();
|