tango-app-api-analysis-traffic 3.8.7-vms.2 → 3.8.7-vms.20

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.2",
3
+ "version": "3.8.7-vms.20",
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.23",
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"
@@ -1,7 +1,7 @@
1
1
  import { logger, insertOpenSearchData, getOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware';
2
2
  import { findOnerevopConfig } from '../services/revopConfig.service.js';
3
3
  import * as clientService from '../services/clients.services.js';
4
- import { bulkUpdate, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
4
+ import { bulkUpdate, insertWithId, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
5
5
  import { findOneVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
6
6
  // import dayjs from 'dayjs';
7
7
  // Lamda Service Call //
@@ -221,6 +221,9 @@ export async function storeProcessedData( req, res ) {
221
221
  dayjs.extend( isSameOrBefore );
222
222
 
223
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' );
224
227
  let end = dayjs( toDate );
225
228
 
226
229
  if ( !start.isValid() || !end.isValid() ) {
@@ -267,10 +270,12 @@ export async function storeProcessedData( req, res ) {
267
270
 
268
271
  const responseArray = [];
269
272
 
270
- for ( let i = 0; i < orderedDates.length; i++ ) {
273
+ for ( let i = 1; i < orderedDates.length; i++ ) {
271
274
  const currentDate = orderedDates[i];
272
275
  const currentId = `${storeId}_${currentDate}`;
276
+ logger.info( { currentId, currentDate } );
273
277
  const processedData = hitsMap.get( currentId );
278
+ logger.info( { processedData } );
274
279
  if ( !processedData ) {
275
280
  responseArray.push( {
276
281
  date: currentDate,
@@ -283,13 +288,17 @@ export async function storeProcessedData( req, res ) {
283
288
 
284
289
  const prevDate = dayjs( currentDate ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
285
290
  const prevId = `${storeId}_${prevDate}`;
291
+ logger.info( { prevId, prevDate } );
286
292
  const previousData = hitsMap.get( prevId );
287
293
 
288
294
  let footfallCountTrend = 0;
295
+ logger.info( { previousData, previoucubr1: previousData?.footfall_count } );
289
296
  if ( previousData && previousData.footfall_count ) {
297
+ logger.info( { previousData, previoucubr: previousData?.footfall_count } );
290
298
  footfallCountTrend = Math.round(
291
- ( ( processedData.footfall_count - previousData.footfall_count ) / previousData.footfall_count ) * 100,
299
+ ( ( processedData.footfall_count - previousData?.footfall_count ) / previousData.footfall_count ) * 100,
292
300
  );
301
+ logger.info( { footfallCountTrend } );
293
302
  }
294
303
  // Add ticket status from openSearch.footfallDirectory (_source.status)
295
304
  let ticketStatus = null;
@@ -302,16 +311,28 @@ export async function storeProcessedData( req, res ) {
302
311
  { term: { 'storeId.keyword': storeId } },
303
312
  { term: { 'dateString': currentDate } },
304
313
  { term: { 'ticketName.keyword': 'footfall-directory' } },
314
+ {
315
+ 'term': {
316
+ 'type.keyword': 'store',
317
+ },
318
+ },
305
319
  ],
306
320
  },
307
321
  },
308
322
  size: 1,
309
- _source: [ 'status' ],
323
+ _source: [ 'status', 'mappingInfo' ],
310
324
  };
311
325
  try {
312
326
  const footfallDirRes = await getOpenSearchData( openSearch.footfallDirectory, footfallDirQuery );
313
327
  const hit = footfallDirRes?.body?.hits?.hits?.[0];
314
- ticketStatus = hit?._source?.status || null;
328
+ if ( hit?._source?.mappingInfo && Array.isArray( hit._source.mappingInfo ) ) {
329
+ for ( let i = 0; i < hit._source.mappingInfo.length; i++ ) {
330
+ if ( hit._source.mappingInfo[i].type === 'tagging' ) {
331
+ ticketStatus = hit._source.mappingInfo[i].status;
332
+ break;
333
+ }
334
+ }
335
+ }
315
336
  } catch ( err ) {
316
337
  logger.warn( { message: 'Could not get ticket status from footfallDirectory', error: err } );
317
338
  }
@@ -335,7 +356,27 @@ export async function storeProcessedData( req, res ) {
335
356
  downtime: processedData?.down_time || 0,
336
357
  ticketStatus,
337
358
  raisedStatusEnabled,
359
+
338
360
  } );
361
+
362
+ if ( raisedStatusEnabled === 'block' ) {
363
+ // Calculate the number of days from currentDate + 1 to the end of the month
364
+ // Assume currentDate is in format 'YYYY-MM-DD'
365
+ const currentDateObj = new Date( currentDate );
366
+ // Move to next day
367
+ const nextDay = new Date( currentDateObj );
368
+ nextDay.setDate( currentDateObj.getDate() + 1 );
369
+
370
+ // Get the last date of the current month
371
+ const endOfMonth = new Date( currentDateObj.getFullYear(), currentDateObj.getMonth() + 1, 0 );
372
+
373
+ // Calculate number of days (inclusive of end date, exclusive of nextDay)
374
+ let noOfBlockedDays = Math.floor( ( endOfMonth - nextDay ) / ( 1000 * 60 * 60 * 24 ) ) + 1;
375
+ if ( noOfBlockedDays < 0 ) noOfBlockedDays = 0;
376
+
377
+ // Add to response
378
+ responseArray[responseArray.length - 1].noOfBlockedDays = noOfBlockedDays;
379
+ }
339
380
  }
340
381
 
341
382
  return res.sendSuccess( responseArray );
@@ -376,43 +417,176 @@ export async function footFallImages( req, res ) {
376
417
  'ticketName.keyword': 'footfall-directory',
377
418
  },
378
419
  },
420
+ {
421
+ 'term': {
422
+ 'type.keyword': 'store',
423
+ },
424
+ },
379
425
  ],
380
426
  },
381
427
  },
382
- // '_source': [ 'dateString', 'storeId', 'duplicateCount', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'junkCount', 'status', 'ticketId', 'comments', 'userName', 'role', 'createdAt', 'email', 'houseKeepingACCount', 'duplicateACCount', 'employeeACCount', 'junkACCount', 'approverEmail', 'approverRole', 'approverUserName' ],
428
+ '_source': [ 'dateString', 'storeId', 'mappingInfo', 'revicedFootfall', 'revicedPerc', 'createdAt', 'updatedAt', 'footfallCount' ],
383
429
 
384
430
  };
385
431
 
386
432
  const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
387
433
  const ticketDetails = getData?.body?.hits?.hits[0];
388
434
  let temp = [];
389
- ticketDetails?._source? temp.push( ticketDetails?._source ) :null;
390
- // temp[0].status = 'open';
391
- if ( ticketDetails?._source?.status == 'closed' ) {
392
- delete temp[0].status;
393
- temp.push( { ...ticketDetails?._source, status: 'closed' } );
394
-
395
- temp[1].userName = getData?.body?.hits?.hits?.[0]?._source?.approverUserName;
396
- temp[1].email = getData?.body?.hits?.hits?.[0]?._source?.approverEmail;
397
- temp[1].role = getData?.body?.hits?.hits?.[0]?._source?.approverRole;
398
- temp[1].employeeCount = getData?.body?.hits?.hits?.[0]?._source?.employeeACCount;
399
- temp[1].houseKeepingCount = getData?.body?.hits?.hits?.[0]?._source?.houseKeepingACCount;
400
- temp[1].duplicateCount = getData?.body?.hits?.hits?.[0]?._source?.duplicateACCount;
401
- temp[1].junkCount = getData?.body?.hits?.hits?.[0]?._source?.junkACCount;
402
- }
403
- const LamdaURL = revop.getImages;
404
- let resultData = await LamdaServiceCall( LamdaURL, inputData );
405
- logger.info( { resultData: resultData } );
406
- if ( resultData ) {
407
- temp.length? temp[0].status = 'open': null;
408
- if ( resultData.status_code == '200' ) {
409
- return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0? temp : null, config: req?.store?.footfallDirectoryConfigs } );
410
- } else {
411
- return res.sendError( 'No Content', 204 );
412
- }
413
- } else {
414
- return res.sendError( 'No Content', 204 );
415
- }
435
+ const footfallValue = ticketDetails?._source?.footfallCount ?? 0;
436
+ const mappingInfoArray = ticketDetails?._source?.mappingInfo ?? [];
437
+ // Helper to get mappingInfo for an actionType
438
+ function getMappingForType( type ) {
439
+ return mappingInfoArray.find( ( m ) => m.type === type ) || {};
440
+ }
441
+
442
+ // List of actionTypes to process in sequence
443
+
444
+ if ( req.user.userType !== 'tango' && req.user.role !== 'superadmin' ) {
445
+ switch ( req.user.role ) {
446
+ case 'user':
447
+ const actionTypesUser = [ 'tagging', 'finalRevision' ];
448
+
449
+ temp = [];
450
+ actionTypesUser.forEach( ( type ) => {
451
+ const mapping = getMappingForType( type );
452
+ if ( type === 'tagging' ) {
453
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
454
+ const revisedPerc =
455
+ footfallValue > 0 ?
456
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
457
+ '0';
458
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
459
+ temp.push( {
460
+ actionType: type,
461
+ footfall: footfallValue,
462
+ revicedFootfall: revisedFootfall,
463
+ revicedPerc: revisedPerc,
464
+ count: countObj,
465
+ createdAt: mapping.createdAt ?? '',
466
+ createdByEmail: mapping.createdByEmail ?? '',
467
+ createdByUserName: mapping.createdByUserName ?? '',
468
+ createdByRole: mapping.createdByRole ?? '',
469
+ isUp: false,
470
+ } );
471
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
472
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
473
+ const revisedPerc =
474
+ footfallValue > 0 ?
475
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
476
+ '0';
477
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
478
+ temp.push( {
479
+ actionType: type,
480
+ footfall: footfallValue,
481
+ revicedFootfall: revisedFootfall,
482
+ revicedPerc: revisedPerc,
483
+ count: countObj,
484
+ createdAt: mapping.createdAt ?? '',
485
+ createdByEmail: mapping.createdByEmail ?? '',
486
+ createdByUserName: mapping.createdByUserName ?? '',
487
+ createdByRole: mapping.createdByRole ?? '',
488
+ } );
489
+ }
490
+ } );
491
+ break;
492
+ case 'admin':
493
+ const actionTypesAdmin = [ 'tagging', 'review', 'finalRevision' ];
494
+ temp = [];
495
+ actionTypesAdmin.forEach( ( type ) => {
496
+ const mapping = getMappingForType( type );
497
+ if ( type === 'tagging' ) {
498
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
499
+ const revisedPerc =
500
+ footfallValue > 0 ?
501
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
502
+ '0';
503
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
504
+ temp.push( {
505
+ actionType: type,
506
+ footfall: footfallValue,
507
+ revicedFootfall: revisedFootfall,
508
+ revicedPerc: revisedPerc,
509
+ count: countObj,
510
+ createdAt: mapping.createdAt ?? '',
511
+ createdByEmail: mapping.createdByEmail ?? '',
512
+ createdByUserName: mapping.createdByUserName ?? '',
513
+ createdByRole: mapping.createdByRole ?? '',
514
+ isUp: false,
515
+ } );
516
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
517
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
518
+ const revisedPerc =
519
+ footfallValue > 0 ?
520
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
521
+ '0';
522
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
523
+ temp.push( {
524
+ actionType: type,
525
+ footfall: footfallValue,
526
+ revicedFootfall: revisedFootfall,
527
+ revicedPerc: revisedPerc,
528
+ count: countObj,
529
+ createdAt: mapping.createdAt ?? '',
530
+ createdByEmail: mapping.createdByEmail ?? '',
531
+ createdByUserName: mapping.createdByUserName ?? '',
532
+ createdByRole: mapping.createdByRole ?? '',
533
+ } );
534
+ }
535
+ } );
536
+ }
537
+ } else {
538
+ const actionTypes = [ 'tagging', 'review', 'approve', 'tangoreview', 'finalRevision' ];
539
+
540
+ // Dynamically add to temp only if actionType matches and status is 'closed',
541
+ // except for 'tagging' where status must be 'raised'
542
+ temp = [];
543
+ actionTypes.forEach( ( type ) => {
544
+ const mapping = getMappingForType( type );
545
+ if ( type === 'tagging' ) {
546
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
547
+ temp.push( {
548
+ actionType: type,
549
+ footfall: footfallValue,
550
+ revicedFootfall: mapping.revicedFootfall ?? 0,
551
+ revicedPerc: mapping.revisedPerc ?? '--',
552
+ count: countObj,
553
+ createdAt: mapping.createdAt ?? '',
554
+ createdByEmail: mapping.createdByEmail ?? '',
555
+ createdByUserName: mapping.createdByUserName ?? '',
556
+ createdByRole: mapping.createdByRole ?? '',
557
+ isUp: false,
558
+ } );
559
+ }
560
+ if ( type !== 'tagging' && mapping.status === 'Closed' ) {
561
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
562
+ temp.push( {
563
+ actionType: type,
564
+ footfall: footfallValue,
565
+ revicedFootfall: mapping.revicedFootfall ?? 0,
566
+ revicedPerc: mapping.revisedPerc ?? '--',
567
+ count: countObj,
568
+ createdAt: mapping.createdAt ?? '',
569
+ createdByEmail: mapping.createdByEmail ?? '',
570
+ createdByUserName: mapping.createdByUserName ?? '',
571
+ createdByRole: mapping.createdByRole ?? '',
572
+ } );
573
+ }
574
+ } );
575
+ }
576
+
577
+
578
+ const LamdaURL = revop.getImages;
579
+ let resultData = await LamdaServiceCall( LamdaURL, inputData );
580
+ if ( resultData ) {
581
+ // temp.length? temp[0].status = 'open': null;
582
+ if ( resultData.status_code == '200' ) {
583
+ return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0 && ticketDetails? temp : null, config: req?.store?.footfallDirectoryConfigs } );
584
+ } else {
585
+ return res.sendError( 'No Content', 204 );
586
+ }
587
+ } else {
588
+ return res.sendError( 'No Content', 204 );
589
+ }
416
590
  } catch ( error ) {
417
591
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
418
592
  const err = error.message || 'Internal Server Error';
@@ -437,9 +611,21 @@ export async function tagTempId( req, res ) {
437
611
  exitTime: inputData.exitTime,
438
612
  filePath: inputData.filePath,
439
613
  status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
440
- description: '',
614
+ description: inputData.comments || '',
441
615
  isChecked: inputData.isChecked,
442
- 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
+ actions: [
622
+ {
623
+ actionType: 'tagging',
624
+ action: 'submitted',
625
+ },
626
+ ],
627
+ } ) ) :
628
+ [],
443
629
  type: 'tagging-reflect',
444
630
  ticketStatus: 'submitted',
445
631
  isParent: inputData?.revopsType === 'duplicate'? true : false,
@@ -449,6 +635,7 @@ export async function tagTempId( req, res ) {
449
635
  action: 'submitted',
450
636
  },
451
637
  ],
638
+ comments: inputData.comments || '',
452
639
  createdAt: new Date(),
453
640
  updatedAt: new Date(),
454
641
 
@@ -464,8 +651,10 @@ export async function tagTempId( req, res ) {
464
651
  ctx._source.description = params.description;
465
652
  ctx._source.isChecked = params.isChecked;
466
653
  ctx._source.duplicateImage = params.duplicateImage;
654
+ ctx._source.isParent = params.isParent;
467
655
  ctx._source.updatedAt = params.updatedAt;
468
656
  ctx._source.timeRange = params.timeRange;
657
+ ctx._source.comments = params.comments;
469
658
  if (ctx._source.createdAt == null) {
470
659
  ctx._source.createdAt = params.createdAt;
471
660
  ctx._source.clientId = params.clientId;
@@ -480,6 +669,32 @@ export async function tagTempId( req, res ) {
480
669
  };
481
670
  const id = `${inputData.storeId}_${inputData.dateString}_${inputData.timeRange}_${inputData.tempId}`;
482
671
  await upsertWithScript( openSearch.revop, id, { script, upsert: upsertRecord } );
672
+ if ( inputData?.comments && inputData?.comments !== '' ) {
673
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
674
+ const logs = {
675
+ type: 'tagging',
676
+ parent: inputData?.revopsType == 'duplicate'? inputData.tempId : null,
677
+ id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
678
+ tempId: inputData.tempId,
679
+ timeRange: inputData.timeRange,
680
+ storeId: inputData.storeId,
681
+ dateString: inputData.dateString,
682
+ processType: inputData.processType,
683
+ category: inputData.revopsType,
684
+ entryTime: inputData.entryTime,
685
+ exitTime: inputData.exitTime,
686
+ filePath: inputData.filePath,
687
+ status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
688
+ description: inputData.comments || '',
689
+ isChecked: inputData.isChecked,
690
+ createdByEmail: req?.user?.email,
691
+ createdByUserName: req?.user?.userName,
692
+ createdByRole: req?.user?.role,
693
+ message: inputData.comments || '',
694
+ createdAt: new Date(),
695
+ };
696
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
697
+ }
483
698
  if ( inputData?.duplicateImage?.length> 0 ) {
484
699
  let bulkBody = [];
485
700
  for ( let item of inputData?.duplicateImage ) {
@@ -487,7 +702,7 @@ export async function tagTempId( req, res ) {
487
702
  clientId: inputData.storeId.split( '-' )[0],
488
703
  storeId: inputData.storeId,
489
704
  tempId: item.tempId,
490
- id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
705
+ id: `${inputData?.storeId}_${inputData?.dateString}_${item?.tempId}`,
491
706
  dateString: inputData.dateString,
492
707
  timeRange: item.timeRange,
493
708
  isChecked: item.isChecked,
@@ -502,6 +717,7 @@ export async function tagTempId( req, res ) {
502
717
  isParent: false,
503
718
  type: 'tagging-reflect',
504
719
  ticketStatus: 'submitted',
720
+ comments: inputData.comments || '',
505
721
  actions: [
506
722
  {
507
723
  actionType: 'tagging',
@@ -3129,29 +3129,30 @@ export const managerTrafficDensityExport = async ( req, res ) => {
3129
3129
  }
3130
3130
  };
3131
3131
 
3132
- // async function getGeocodedAddress( lat, lng ) {
3133
- // try {
3134
- // const apiKey = 'AIzaSyDlOezgwQO0JviD0aizrCuN1FY9tcWfR3o'; // Use this if you're using dotenv
3135
- // const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${apiKey}`;
3136
-
3137
- // const requestOptions = {
3138
- // method: 'GET',
3139
- // headers: {
3140
- // 'Content-Type': 'application/json',
3141
- // },
3142
- // };
3143
- // const response = await fetch( url, requestOptions );
3144
- // console.log( 'response =>', response );
3145
- // if ( !response.ok ) {
3146
- // throw new Error( `Response status: ${response.status}` );
3147
- // return false;
3148
- // }
3149
- // const json = await response.json();
3150
- // return json;
3151
- // } catch ( error ) {
3152
- // console.log( 'getGeocodedAddress error =>', error );
3153
- // logger.error( { error: error, message: data, function: 'getGeocodedAddress' } );
3154
- // }
3155
- // }
3156
- // let getGEO = await getGeocodedAddress( 12.900260100404893, 80.23384232089138 );
3157
- // console.log( 'getGEO =>', getGEO );
3132
+
3133
+ export const getStoreListv2 = async ( req, res ) => {
3134
+ try {
3135
+ let reqestData = req.body;
3136
+ let getClientData = await getClientConfig( reqestData.clientId );
3137
+ if ( !getClientData ) {
3138
+ return res.sendError( 'Invalid Client Id', 400 );
3139
+ }
3140
+ reqestData.featureConfigs = getClientData.featureConfigs;
3141
+ let LamdaURL = 'https://nmnnlq3ie65bljcvxauhxmsrpa0edhfa.lambda-url.ap-south-1.on.aws/';
3142
+ let resultData = await LamdaServiceCall( LamdaURL, reqestData );
3143
+ if ( resultData ) {
3144
+ if ( resultData.status_code == '200' ) {
3145
+ return res.sendSuccess( resultData );
3146
+ } else {
3147
+ return res.sendError( 'No Content', 204 );
3148
+ }
3149
+ } else {
3150
+ return res.sendError( 'No Content', 204 );
3151
+ }
3152
+ } catch ( error ) {
3153
+ const err = error.message || 'Internal Server Error';
3154
+ logger.error( { error: error, message: req.body, function: 'getStoreListv2' } );
3155
+ return res.sendError( err, 500 );
3156
+ }
3157
+ };
3158
+
@@ -56,8 +56,8 @@ export const tagTempIdSchema = joi.object( {
56
56
 
57
57
  const diff = today.diff( inputDate, 'day' );
58
58
 
59
- if ( diff > 7 ) {
60
- return helpers.message( 'Tagging is not allowed for a period exceeding 7 days' );
59
+ if ( diff > 3 ) {
60
+ return helpers.message( 'Tagging is not allowed for a period exceeding 3 days' );
61
61
  }
62
62
 
63
63
  return value;
@@ -84,6 +84,7 @@ export const tagTempIdSchema = joi.object( {
84
84
  entryTime: joi.string().required(),
85
85
  exitTime: joi.string().required(),
86
86
  filePath: joi.string().required(),
87
+ comments: joi.string().optional().allow( '' ),
87
88
 
88
89
  } );
89
90
 
@@ -239,6 +239,17 @@ export const validateCountryHeaderSchemav2 = joi.object( {
239
239
  export const validateCountryHeaderParamsv2 = {
240
240
  body: validateCountryHeaderSchemav2,
241
241
  };
242
+
243
+ export const validateGetStoreListSchemav2 = joi.object( {
244
+ clientId: joi.string().required(),
245
+ storeId: joi.array().items( joi.string().required() ).required(),
246
+ downtime: joi.array().items( joi.string().required() ).required(),
247
+ fromDate: joi.string().required(),
248
+ toDate: joi.string().required(),
249
+ } );
250
+ export const validateGetStoreList = {
251
+ body: validateGetStoreListSchemav2,
252
+ };
242
253
  export const getMyProductSchema = joi.object( {
243
254
  clientId: joi.string().required(),
244
255
  storeId: joi.array().optional().empty(),
@@ -18,7 +18,6 @@ revopRouter
18
18
  .get( '/footfall-images', isAllowedSessionHandler, validate( footfallImagesValid ), getTaggingConfig, footFallImages )
19
19
  .post( '/tag-tempId', isAllowedSessionHandler, validate( tagTempIdValid ), deleteTaggedDuplicate, mappingConfig, tagTempId )
20
20
 
21
-
22
21
  .post( '/get-categorized-images', isAllowedSessionHandler, validate( getCategorizedImagesValid ), getCategorizedImages );
23
22
 
24
23
 
@@ -92,6 +92,7 @@ import {
92
92
  funnelV3,
93
93
  getStoreMapDataV3,
94
94
  managerTrafficDensityExport,
95
+ getStoreListv2,
95
96
  } from '../controllers/tangoTrafficV3.controllers.js';
96
97
  analysisTrafficRouter
97
98
  .get( '/welcome', welcome )
@@ -175,5 +176,6 @@ analysisTrafficRouter
175
176
  .post( '/headerCountry_v2', isAllowedSessionHandler, isAllowedClient, validate( validationDtos.validateCountryHeaderParamsv2 ), getAssinedStore, headerCountryV2 )
176
177
  .post( '/checkTodayReportStatus', isAllowedSessionHandler, checkTodayReportStatus )
177
178
  .post( '/headerZoneV2', isAllowedSessionHandler, headerZoneV2 )
178
- .post( '/trafficDensityExport_v2', managerTrafficDensityExport );
179
+ .post( '/trafficDensityExport_v2', managerTrafficDensityExport )
180
+ .post( '/get-store-list-v2', isAllowedSessionHandler, isAllowedClient, validate( validationDtos.validateGetStoreList ), getStoreListv2 );
179
181
  export default analysisTrafficRouter;
@@ -1,6 +1,5 @@
1
1
  import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
2
- import { findOneStore } from '../services/stores.service.js';
3
- import { deleteByQuery } from 'tango-app-api-middleware/src/utils/openSearch.js';
2
+ import { deleteByQuery, getOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
4
3
  import { findOne } from '../services/clients.services.js';
5
4
 
6
5
  export async function getTaggingConfig( req, res, next ) {
@@ -65,41 +64,54 @@ export async function getFootfallCount( req, res, next ) {
65
64
  export async function mappingConfig( req, res, next ) {
66
65
  try {
67
66
  const inputData = req.body;
67
+
68
68
  const openSearch = JSON.parse( process.env.OPENSEARCH );
69
- const config =await findOneStore( { storeId: inputData.storeId }, { revopTagging: 1 } );
70
- if ( inputData.revopsType == 'employee' ) {
71
- const getQuery = {
72
- query: {
73
- bool: {
74
- must: [
75
- {
76
- term: {
77
- 'storeId.keyword': inputData.storeId,
78
- },
79
- },
80
- {
81
- term: {
82
- 'dateString': inputData.dateString,
83
- },
69
+ const footfallQuery ={
70
+ query: {
71
+ bool: {
72
+ must: [
73
+ {
74
+ term: {
75
+ 'store_id.keyword': inputData.storeId,
84
76
  },
85
- {
86
- term: {
87
- 'revopsType.keyword': inputData.revopsType,
88
- },
77
+ },
78
+ {
79
+ term: {
80
+ 'date_string': inputData.dateString,
89
81
  },
90
- ],
91
- },
82
+ },
83
+
84
+ ],
92
85
  },
93
- };
86
+ },
87
+ };
94
88
 
95
- const getData = await getOpenSearchCount( openSearch.revop, getQuery );
96
- if ( getData && getData?.body?.count >= config?.revopTagging?.employee ) {
97
- return res.sendError( `Select up to ${config?.revopTagging?.employee} items only`, 400 );
98
- } else {
99
- next();
100
- }
101
- } else if ( inputData.revopsType == 'house-keeping' ) {
102
- const getQuery = {
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 = {
103
115
  query: {
104
116
  bool: {
105
117
  must: [
@@ -118,92 +130,51 @@ export async function mappingConfig( req, res, next ) {
118
130
  'revopsType.keyword': inputData.revopsType,
119
131
  },
120
132
  },
133
+ {
134
+ term: {
135
+ 'isParent': false,
136
+ },
137
+ },
121
138
  ],
122
139
  },
123
140
  },
124
141
  };
125
- const getData = await getOpenSearchCount( openSearch.revop, getQuery );
126
- if ( getData && getData?.body?.count >= config?.revopTagging?.houseKeeping ) {
127
- return res.sendError( `Select up to ${config?.revopTagging?.houseKeeping} items only`, 400 );
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
+ } );
128
160
  } else {
129
- next();
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
+ } );
130
170
  }
131
- } else if ( inputData.revopsType === 'junk' ) {
132
- if ( ( req?.user?.userType == 'client' && req?.user?.userType !== 'user ' ) || req?.user?.userType === 'tango' ) {
133
- next();
134
- return;
135
- } else {
136
- 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 );
137
173
  }
174
+ return next();
138
175
  } else {
139
- next();
176
+ return next();
140
177
  }
141
- // else if ( inputData.revopsType == 'duplicate' ) {
142
- // const getFootfallQuery = {
143
- // query: {
144
- // terms: {
145
- // _id: [ inputData?.dateString ],
146
- // },
147
- // },
148
- // _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
149
- // sort: [
150
- // {
151
- // date_iso: {
152
- // order: 'desc',
153
- // },
154
- // },
155
- // ],
156
- // };
157
-
158
- // const getFootfall = await getOpenSearchData( openSearch.footfall, getFootfallQuery );
159
- // const footfall = getFootfall?.body?.hites?.hits?.[0]?._source?.footfall_count;
160
- // const getQuery = {
161
- // query: {
162
- // bool: {
163
- // must: [
164
- // {
165
- // term: {
166
- // 'storeId.keyword': inputData.storeId,
167
- // },
168
- // },
169
- // {
170
- // term: {
171
- // 'dateString': inputData.dateString,
172
- // },
173
- // },
174
- // {
175
- // term: {
176
- // 'revopsType.keyword': inputData.revopsType,
177
- // },
178
- // },
179
- // {
180
- // term: {
181
- // 'parent.keyword': null,
182
- // },
183
- // },
184
- // ],
185
- // },
186
- // },
187
- // };
188
- // const getData = await getOpenSearchCount( openSearch.revop, getQuery );
189
- // logger.info( { getData: getData, footfall: footfall, duplicate: config?.revopTagging?.duplicate } );
190
- // if ( getData && footfall && config?.revopTagging?.duplicate ) {
191
- // const data = config?.revopTagging?.duplicate;
192
- // // Convert "20%" → 0.2 (handle both "20%" and 20)
193
- // const percentStr = typeof data === 'string' ? data.replace( '%', '' ) : data;
194
- // logger.info( { percentStr: percentStr } );
195
- // const percentValue =percentStr / 100;
196
-
197
- // const result = percentValue * footfall;
198
-
199
- // logger.info( { result: result, footfall: footfall } );
200
- // }
201
- // if ( getData && getData?.body?.count >= Math.round( result ) ) {
202
- // return res.sendError( `Select up to ${config?.revopTagging?.duplicate} items only`, 400 );
203
- // } else {
204
- // next();
205
- // }
206
- // }
207
178
  } catch ( error ) {
208
179
  logger.error( { error: error, message: req.body, function: 'traffic-revop-getTaggingConfig' } );
209
180
  next();