tango-app-api-analysis-traffic 3.8.9 → 3.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
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, scrollResponse, searchOpenSearchData, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
5
+ import { findOneVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
5
6
  import dayjs from 'dayjs';
6
7
  // Lamda Service Call //
7
8
  async function LamdaServiceCall( url, data ) {
@@ -16,7 +17,6 @@ async function LamdaServiceCall( url, data ) {
16
17
  const response = await fetch( url, requestOptions );
17
18
  if ( !response.ok ) {
18
19
  throw new Error( `Response status: ${response.status}` );
19
- return false;
20
20
  }
21
21
  const json = await response.json();
22
22
  return json;
@@ -69,7 +69,7 @@ export async function revoptagging( req, res ) {
69
69
 
70
70
  let respo= await getOpenSearchData( openSearch.revops, searchQuery );
71
71
  const revopData = respo?.body?.hits?.hits;
72
- if ( revopData&& revopData.length>0 ) {
72
+ if ( revopData && revopData.length>0 ) {
73
73
  await updateOpenSearchData( openSearch.revops, revopData[0]._id, { doc: item } );
74
74
  } else {
75
75
  item.createdAt = new Date();
@@ -92,7 +92,7 @@ async function getClientConfig( clientId ) {
92
92
  }
93
93
  return getClientData;
94
94
  } catch ( error ) {
95
- logger.error( { error: error, message: data, function: 'getClientConfig' } );
95
+ logger.error( { error: error, message: clientId, function: 'getClientConfig' } );
96
96
  return false;
97
97
  }
98
98
  }
@@ -126,7 +126,7 @@ export async function getrevoptagging( req, res ) {
126
126
  };
127
127
  let respo= await getOpenSearchData( openSearch.revops, searchQuery );
128
128
  const revopData = respo?.body?.hits?.hits;
129
- if ( revopData.length>0 ) {
129
+ if ( revopData && revopData.length>0 ) {
130
130
  return res.sendSuccess( revopData[0]._source );
131
131
  } else {
132
132
  return res.sendError( 'no data found', 204 );
@@ -136,6 +136,690 @@ export async function getrevoptagging( req, res ) {
136
136
  return res.sendError( { error: error }, 500 );
137
137
  }
138
138
  }
139
+
140
+ export async function migrateRevopIndex( req, res ) {
141
+ try {
142
+ const { storeId, dateString, size = 100 } = req.body;
143
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
144
+
145
+ const query = {
146
+ size: size,
147
+ query: {
148
+ bool: {
149
+ must: [
150
+ // {
151
+ // range: {
152
+ // createdAt: {
153
+ // gte: '2025-10-01T00:00:00.000Z',
154
+ // lte: '2025-10-31T23:59:59.000Z',
155
+ // },
156
+ // },
157
+ // },
158
+ {
159
+ term: {
160
+ 'type.keyword': 'tagging-reflect',
161
+
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ },
167
+ };
168
+
169
+ if ( storeId ) {
170
+ query.query.bool.must.push( {
171
+ term: {
172
+ 'storeId.keyword': storeId,
173
+ },
174
+ } );
175
+ }
176
+
177
+ if ( dateString ) {
178
+ query.query.bool.must.push( {
179
+ terms: {
180
+ dateString: Array.isArray( dateString ) ? dateString : `${dateString}`.split( ',' ),
181
+ },
182
+ } );
183
+ }
184
+
185
+ // const response = await getOpenSearchData( openSearch.revop, query );
186
+ // const hits = response?.body?.hits?.hits || [];
187
+
188
+ // Use OpenSearch scroll API to retrieve up to 60000 records efficiently
189
+ let allHits = [];
190
+ let scrollId = null;
191
+ let totalFetched = 0;
192
+ let firstResponse = await searchOpenSearchData( openSearch.revop, query );
193
+ // Collect first batch
194
+ let hitsBatch = firstResponse?.body?.hits?.hits || [];
195
+ if ( hitsBatch.length > 0 ) {
196
+ allHits.push( ...hitsBatch );
197
+ totalFetched += hitsBatch.length;
198
+ scrollId = firstResponse.body._scroll_id;
199
+ }
200
+
201
+ const bulkBody = [];
202
+ for ( const hit of hitsBatch ) {
203
+ const src = hit._source || {};
204
+ const statusValue = ( src.status || '' ).toLowerCase();
205
+ // const parentValue = src.parent;
206
+
207
+ // Get ticket sattasu from the footfalldirectory index matching src.storeId and src.dateString
208
+ let ticketStatus = null;
209
+
210
+ const footfallQuery = {
211
+ size: 1,
212
+ query: {
213
+ bool: {
214
+ must: [
215
+ { term: { 'storeId.keyword': src.storeId } },
216
+ { term: { 'dateString': src.dateString } },
217
+ ],
218
+ },
219
+ },
220
+ };
221
+ const footfallResp = await getOpenSearchData( openSearch.oldFootfallDirectory, footfallQuery );
222
+ ticketStatus = footfallResp?.body?.hits?.hits?.[0]?._source?.status || null;
223
+ if ( src?.duplicateImage?.length > 0 ) {
224
+ src.duplicateImage = src.duplicateImage.map( ( item ) => ( {
225
+ ...item,
226
+ id: `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${item.tempId || ''}`,
227
+ actions: ( ticketStatus === 'closed' && item.isChecked === true ) ? [
228
+ {
229
+ actionType: 'tagging',
230
+ action: 'submitted',
231
+ },
232
+ {
233
+ actionType: 'review',
234
+ action: 'approved',
235
+ },
236
+ ]: ( ticketStatus === 'closed' && item.isChecked === false )?
237
+ [
238
+ {
239
+ actionType: 'tagging',
240
+ action: 'submitted',
241
+ },
242
+ {
243
+ actionType: 'review',
244
+ action: 'rejected',
245
+ },
246
+ ]:
247
+ [
248
+ {
249
+ actionType: 'tagging',
250
+ action: 'submitted',
251
+ },
252
+ ],
253
+ // Include relevant action, assuming 'actions' will be determined below
254
+ } ) );
255
+ }
256
+
257
+
258
+ const idValue = `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${src.tempId || ''}`;
259
+ let actions = [
260
+ {
261
+ actionType: 'tagging',
262
+ action: 'submitted',
263
+ },
264
+ ];
265
+
266
+ if ( statusValue === 'approved' ) {
267
+ actions = [
268
+ {
269
+ actionType: 'tagging',
270
+ action: 'submitted',
271
+ },
272
+ {
273
+ actionType: 'review',
274
+ action: 'approved',
275
+ },
276
+ ];
277
+ } else if ( statusValue === 'rejected' ) {
278
+ actions = [
279
+ {
280
+ actionType: 'tagging',
281
+ action: 'submitted',
282
+ },
283
+ {
284
+ actionType: 'review',
285
+ action: 'rejected',
286
+ },
287
+ ];
288
+ }
289
+
290
+
291
+ const doc = {
292
+ ...src,
293
+ id: idValue,
294
+ revopsType: src?.revopsType === 'house-keeping'? 'houseKeeping' : src?.revopsType,
295
+ isParent: src?.duplicateImage?.length > 0? true : false,
296
+ actions,
297
+ ticketStatus: src.status,
298
+ // updatedAt: new Date(),
299
+ };
300
+
301
+ bulkBody.push(
302
+ { update: { _index: openSearch.newRevop, _id: hit._id } },
303
+ { doc: doc, doc_as_upsert: true },
304
+ );
305
+ }
306
+
307
+ const bulkRes = await bulkUpdate( bulkBody );
308
+
309
+ if ( bulkRes?.errors ) {
310
+ logger.error( 'Bulk migration errors:', bulkRes.items );
311
+ return res.sendError( 'Failed to migrate some records', 500 );
312
+ }
313
+
314
+ while ( hitsBatch.length > 0 && scrollId ) {
315
+ // Fetch next batch using scroll_id
316
+ const nextScrollRes = await scrollResponse( scrollId );
317
+
318
+ hitsBatch = nextScrollRes?.body?.hits?.hits || [];
319
+ if ( hitsBatch.length === 0 ) break;
320
+ logger.info( { hitsBatch: hitsBatch?.length } );
321
+ const bulkBody = [];
322
+ for ( const hit of hitsBatch ) {
323
+ const src = hit._source || {};
324
+ const statusValue = ( src.status || '' ).toLowerCase();
325
+ // const parentValue = src.parent;
326
+
327
+ // Get ticket sattasu from the footfalldirectory index matching src.storeId and src.dateString
328
+ let ticketStatus = null;
329
+
330
+ const footfallQuery = {
331
+ size: 1,
332
+ query: {
333
+ bool: {
334
+ must: [
335
+ { term: { 'storeId.keyword': src.storeId } },
336
+ { term: { 'dateString': src.dateString } },
337
+ ],
338
+ },
339
+ },
340
+ };
341
+ const footfallResp = await getOpenSearchData( openSearch.oldFootfallDirectory, footfallQuery );
342
+ ticketStatus = footfallResp?.body?.hits?.hits?.[0]?._source?.status || null;
343
+ if ( src?.duplicateImage?.length > 0 ) {
344
+ src.duplicateImage = src.duplicateImage.map( ( item ) => ( {
345
+ ...item,
346
+ id: `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${item.tempId || ''}`,
347
+ actions: ( ticketStatus === 'closed' && item.isChecked === true ) ? [
348
+ {
349
+ actionType: 'tagging',
350
+ action: 'submitted',
351
+ },
352
+ {
353
+ actionType: 'review',
354
+ action: 'approved',
355
+ },
356
+ ]: ( ticketStatus === 'closed' && item.isChecked === false )?
357
+ [
358
+ {
359
+ actionType: 'tagging',
360
+ action: 'submitted',
361
+ },
362
+ {
363
+ actionType: 'review',
364
+ action: 'rejected',
365
+ },
366
+ ]:
367
+ [
368
+ {
369
+ actionType: 'tagging',
370
+ action: 'submitted',
371
+ },
372
+ ],
373
+ // Include relevant action, assuming 'actions' will be determined below
374
+ } ) );
375
+ }
376
+
377
+
378
+ const idValue = `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${src.tempId || ''}`;
379
+ logger.info( { idValue } );
380
+ let actions = [
381
+ {
382
+ actionType: 'tagging',
383
+ action: 'submitted',
384
+ },
385
+ ];
386
+
387
+ if ( statusValue === 'approved' ) {
388
+ actions = [
389
+ {
390
+ actionType: 'tagging',
391
+ action: 'submitted',
392
+ },
393
+ {
394
+ actionType: 'review',
395
+ action: 'approved',
396
+ },
397
+ ];
398
+ } else if ( statusValue === 'rejected' ) {
399
+ actions = [
400
+ {
401
+ actionType: 'tagging',
402
+ action: 'submitted',
403
+ },
404
+ {
405
+ actionType: 'review',
406
+ action: 'rejected',
407
+ },
408
+ ];
409
+ }
410
+
411
+
412
+ const doc = {
413
+ ...src,
414
+ id: idValue,
415
+ revopsType: src?.revopsType === 'house-keeping'? 'houseKeeping' : src?.revopsType,
416
+ isParent: src?.duplicateImage?.length > 0? true : false,
417
+ actions,
418
+ ticketStatus: src.status,
419
+ // updatedAt: new Date(),
420
+ };
421
+
422
+ bulkBody.push(
423
+ { update: { _index: openSearch.newRevop, _id: hit._id } },
424
+ { doc: doc, doc_as_upsert: true },
425
+ );
426
+ }
427
+
428
+ const bulkRes = await bulkUpdate( bulkBody );
429
+
430
+ if ( bulkRes?.errors ) {
431
+ logger.error( 'Bulk migration errors:', bulkRes.items );
432
+ return res.sendError( 'Failed to migrate some records', 500 );
433
+ }
434
+ allHits.push( ...hitsBatch );
435
+ totalFetched += hitsBatch.length;
436
+ logger.info( { totalFetched } );
437
+ // Protect against exceeding limit
438
+
439
+ scrollId = nextScrollRes.body._scroll_id;
440
+ }
441
+
442
+ // For downstream logic, use allHits instead of hits
443
+ const hits = allHits;
444
+
445
+ if ( hits.length === 0 ) {
446
+ return res.sendSuccess( { message: 'No records found for migration', updated: 0 } );
447
+ }
448
+
449
+
450
+ // for ( const hit of hits ) {
451
+ // const src = hit._source || {};
452
+ // const statusValue = ( src.status || '' ).toLowerCase();
453
+ // // const parentValue = src.parent;
454
+
455
+ // // Get ticket sattasu from the footfalldirectory index matching src.storeId and src.dateString
456
+ // let ticketStatus = null;
457
+
458
+ // const footfallQuery = {
459
+ // size: 1,
460
+ // query: {
461
+ // bool: {
462
+ // must: [
463
+ // { term: { 'storeId.keyword': src.storeId } },
464
+ // { term: { 'dateString': src.dateString } },
465
+ // ],
466
+ // },
467
+ // },
468
+ // };
469
+ // const footfallResp = await getOpenSearchData( openSearch.oldFootfallDirectory, footfallQuery );
470
+ // ticketStatus = footfallResp?.body?.hits?.hits?.[0]?._source?.status || null;
471
+ // if ( src?.duplicateImage?.length > 0 ) {
472
+ // src.duplicateImage = src.duplicateImage.map( ( item ) => ( {
473
+ // ...item,
474
+ // id: `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${item.tempId || ''}`,
475
+ // actions: ( ticketStatus === 'closed' && item.isChecked === true ) ? [
476
+ // {
477
+ // actionType: 'tagging',
478
+ // action: 'submitted',
479
+ // },
480
+ // {
481
+ // actionType: 'review',
482
+ // action: 'approved',
483
+ // },
484
+ // ]: ( ticketStatus === 'closed' && item.isChecked === false )?
485
+ // [
486
+ // {
487
+ // actionType: 'tagging',
488
+ // action: 'submitted',
489
+ // },
490
+ // {
491
+ // actionType: 'review',
492
+ // action: 'rejected',
493
+ // },
494
+ // ]:
495
+ // [
496
+ // {
497
+ // actionType: 'tagging',
498
+ // action: 'submitted',
499
+ // },
500
+ // ],
501
+ // // Include relevant action, assuming 'actions' will be determined below
502
+ // } ) );
503
+ // }
504
+
505
+
506
+ // const idValue = `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${src.tempId || ''}`;
507
+
508
+ // let actions = [
509
+ // {
510
+ // actionType: 'tagging',
511
+ // action: 'submitted',
512
+ // },
513
+ // ];
514
+
515
+ // if ( statusValue === 'approved' ) {
516
+ // actions = [
517
+ // {
518
+ // actionType: 'tagging',
519
+ // action: 'submitted',
520
+ // },
521
+ // {
522
+ // actionType: 'review',
523
+ // action: 'approved',
524
+ // },
525
+ // ];
526
+ // } else if ( statusValue === 'rejected' ) {
527
+ // actions = [
528
+ // {
529
+ // actionType: 'tagging',
530
+ // action: 'submitted',
531
+ // },
532
+ // {
533
+ // actionType: 'review',
534
+ // action: 'rejected',
535
+ // },
536
+ // ];
537
+ // }
538
+
539
+
540
+ // const doc = {
541
+ // ...src,
542
+ // id: idValue,
543
+ // revopsType: src?.revopsType === 'house-keeping'? 'houseKeeping' : src?.revopsType,
544
+ // isParent: src?.duplicateImage?.length > 0? true : false,
545
+ // actions,
546
+ // ticketStatus: src.status,
547
+ // // updatedAt: new Date(),
548
+ // };
549
+
550
+
551
+ // bulkBody.push(
552
+ // { update: { _index: openSearch.newRevop, _id: hit._id } },
553
+ // { doc: doc, doc_as_upsert: true },
554
+ // );
555
+ // }
556
+ // Implement batch by batch update
557
+ // const BATCH_SIZE = 10000; // You can adjust the batch size as needed
558
+
559
+ // for ( let i = 0; i < bulkBody.length; i += BATCH_SIZE ) {
560
+ // const batch = bulkBody.slice( i, i + BATCH_SIZE );
561
+ // const bulkRes = await bulkUpdate( batch );
562
+
563
+ // if ( bulkRes?.errors ) {
564
+ // logger.error( 'Bulk migration errors:', bulkRes.items );
565
+ // return res.sendError( 'Failed to migrate some records', 500 );
566
+ // }
567
+ // }
568
+
569
+ // const bulkRes = await bulkUpdate( bulkBody );
570
+
571
+ // if ( bulkRes?.errors ) {
572
+ // logger.error( 'Bulk migration errors:', bulkRes.items );
573
+ // return res.sendError( 'Failed to migrate some records', 500 );
574
+ // }
575
+
576
+ return res.sendSuccess( { message: 'Migration completed', updated: hits.length } );
577
+ } catch ( error ) {
578
+ logger.error( { error: error, message: req.body, function: 'migrateRevopIndex' } );
579
+ return res.sendError( { error: error }, 500 );
580
+ }
581
+ }
582
+
583
+ export async function expireReviewStatus( req, res ) {
584
+ try {
585
+ const {
586
+ thresholdDate,
587
+ batchSize = 500,
588
+ storeId,
589
+ dateString,
590
+ } = req.body;
591
+ const cutoffDate = new Date( thresholdDate );
592
+ // Convert cutoffDate to "2026-01-12T23:59:59.000Z"
593
+
594
+ cutoffDate.setUTCHours( 23, 59, 59, 0 );
595
+
596
+ console.log( cutoffDate );
597
+ if ( Number.isNaN( cutoffDate.getTime() ) ) {
598
+ return res.sendError( 'Invalid thresholdDate', 400 );
599
+ }
600
+
601
+
602
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
603
+ const query = {
604
+ size: batchSize,
605
+ query: {
606
+ bool: {
607
+ must: [
608
+ { term: { 'ticketName.keyword': 'footfall-directory' } },
609
+ { term: { 'type.keyword': 'store' } },
610
+ ],
611
+ must_not: [
612
+ { terms: { 'status.keyword': [ 'Closed' ] } },
613
+ ],
614
+ },
615
+ },
616
+ };
617
+
618
+
619
+ if ( storeId ) {
620
+ query.query.bool.must.push( { term: { 'storeId.keyword': storeId } } );
621
+ }
622
+
623
+
624
+ if ( dateString ) {
625
+ query.query.bool.must.push( {
626
+ terms: {
627
+ dateString: Array.isArray( dateString ) ? dateString : `${dateString}`.split( ',' ),
628
+ },
629
+ } );
630
+ }
631
+
632
+
633
+ let totalUpdated = 0;
634
+ let scrollId = null;
635
+
636
+ let firstResponse = await searchOpenSearchData( openSearch.footfallDirectory, query );
637
+ let hitsBatch = firstResponse?.body?.hits?.hits || [];
638
+ scrollId = firstResponse?.body?._scroll_id;
639
+
640
+ while ( hitsBatch.length > 0 ) {
641
+ const bulkBody = [];
642
+
643
+ for ( const hit of hitsBatch ) {
644
+ const src = hit._source || {};
645
+ const mappingInfo = Array.isArray( src.mappingInfo ) ? src.mappingInfo : [];
646
+ let changed = false;
647
+ let updatedMapping = mappingInfo.map( ( item ) => {
648
+ if ( item?.type === 'review' && item?.status !== 'Closed' && item?.dueDate ) {
649
+ const due = new Date( item.dueDate );
650
+ if ( !Number.isNaN( due.getTime() ) && due < cutoffDate ) {
651
+ changed = true;
652
+
653
+ return { ...item, status: 'Expired' };
654
+ }
655
+ }
656
+
657
+ return item;
658
+ } );
659
+ if ( changed ) {
660
+ updatedMapping = updatedMapping.map( ( item ) => {
661
+ if ( item?.type === 'tagging' ) {
662
+ return { ...item, status: 'Expired' };
663
+ }
664
+ return item;
665
+ } );
666
+ }
667
+
668
+ if ( changed ) {
669
+ const doc = {
670
+ mappingInfo: updatedMapping,
671
+ status: 'Reviewer-Expired',
672
+ };
673
+ logger.info( { updatedMapping } );
674
+ bulkBody.push(
675
+ { update: { _index: openSearch.footfallDirectory, _id: hit._id } },
676
+ { doc: doc, doc_as_upsert: true },
677
+ );
678
+ }
679
+ }
680
+
681
+ if ( bulkBody.length > 0 ) {
682
+ const bulkRes = await bulkUpdate( bulkBody );
683
+ if ( bulkRes?.errors ) {
684
+ logger.error( { message: 'Bulk expire errors', items: bulkRes.items } );
685
+ }
686
+ totalUpdated += bulkBody.length / 2;
687
+ }
688
+ logger.info( { totalUpdated, msg: '........9' } );
689
+ if ( !scrollId ) break;
690
+ const nextRes = await scrollResponse( scrollId );
691
+ hitsBatch = nextRes?.body?.hits?.hits || [];
692
+ scrollId = nextRes?.body?._scroll_id;
693
+ }
694
+
695
+ return res.sendSuccess( { message: 'Expired review status updated', updated: totalUpdated } );
696
+ } catch ( error ) {
697
+ logger.error( { error: error, message: req.body, function: 'expireReviewStatus' } );
698
+ return res.sendError( { error: error }, 500 );
699
+ }
700
+ }
701
+
702
+
703
+ export async function expireApproveStatus( req, res ) {
704
+ try {
705
+ const {
706
+ thresholdDate = '2026-01-21',
707
+ batchSize = 500,
708
+ storeId,
709
+ dateString,
710
+ } = req.body;
711
+ const cutoffDate = new Date( thresholdDate );
712
+ if ( Number.isNaN( cutoffDate.getTime() ) ) {
713
+ return res.sendError( 'Invalid thresholdDate', 400 );
714
+ }
715
+
716
+
717
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
718
+ const query = {
719
+ size: batchSize,
720
+ query: {
721
+ bool: {
722
+ must: [
723
+ { term: { 'ticketName.keyword': 'footfall-directory' } },
724
+ { term: { 'type.keyword': 'store' } },
725
+ ],
726
+ must_not: [
727
+ { terms: { 'status.keyword': [ 'Closed' ] } },
728
+ ],
729
+ },
730
+ },
731
+ };
732
+
733
+
734
+ if ( storeId ) {
735
+ query.query.bool.must.push( { term: { 'storeId.keyword': storeId } } );
736
+ }
737
+
738
+
739
+ if ( dateString ) {
740
+ query.query.bool.must.push( {
741
+ terms: {
742
+ dateString: Array.isArray( dateString ) ? dateString : `${dateString}`.split( ',' ),
743
+ },
744
+ } );
745
+ }
746
+
747
+
748
+ let totalUpdated = 0;
749
+ let scrollId = null;
750
+
751
+ let firstResponse = await searchOpenSearchData( openSearch.footfallDirectory, query );
752
+ let hitsBatch = firstResponse?.body?.hits?.hits || [];
753
+ scrollId = firstResponse?.body?._scroll_id;
754
+
755
+ while ( hitsBatch.length > 0 ) {
756
+ const bulkBody = [];
757
+
758
+ for ( const hit of hitsBatch ) {
759
+ const src = hit._source || {};
760
+ const mappingInfo = Array.isArray( src.mappingInfo ) ? src.mappingInfo : [];
761
+ let changed = false;
762
+ let updatedMapping = mappingInfo.map( ( item ) => {
763
+ if ( item?.type === 'approve' && item?.status !== 'Closed' && item?.dueDate ) {
764
+ const due = new Date( item.dueDate );
765
+ logger.info( { item, due, msg: '..........1', cutoffDate } );
766
+ if ( !Number.isNaN( due.getTime() ) && due < cutoffDate ) {
767
+ changed = true;
768
+
769
+ return { ...item, status: 'Expired' };
770
+ }
771
+ }
772
+
773
+ return item;
774
+ } );
775
+ logger.info( { updatedMapping, msh: '.......12' } );
776
+ if ( changed ) {
777
+ logger.info( { changed, msg: '.......2' } );
778
+ updatedMapping = updatedMapping.map( ( item ) => {
779
+ logger.info( { item, msg: '.......3' } );
780
+ if ( item?.type === 'tagging' ) {
781
+ logger.info( { item: item?.type, msg: '.......4' } );
782
+ return { ...item, status: 'Expired' };
783
+ }
784
+ return item;
785
+ } );
786
+ }
787
+
788
+ logger.info( { updatedMapping, msh: '.......13' } );
789
+ if ( changed ) {
790
+ const doc = {
791
+ mappingInfo: updatedMapping,
792
+ status: 'Expired',
793
+ };
794
+ logger.info( { updatedMapping } );
795
+ bulkBody.push(
796
+ { update: { _index: openSearch.footfallDirectory, _id: hit._id } },
797
+ { doc: doc, doc_as_upsert: true },
798
+ );
799
+ }
800
+ }
801
+
802
+ if ( bulkBody.length > 0 ) {
803
+ const bulkRes = await bulkUpdate( bulkBody );
804
+ if ( bulkRes?.errors ) {
805
+ logger.error( { message: 'Bulk expire errors', items: bulkRes.items } );
806
+ }
807
+ totalUpdated += bulkBody.length / 2;
808
+ }
809
+ logger.info( { totalUpdated, msg: '........9' } );
810
+ if ( !scrollId ) break;
811
+ const nextRes = await scrollResponse( scrollId );
812
+ hitsBatch = nextRes?.body?.hits?.hits || [];
813
+ scrollId = nextRes?.body?._scroll_id;
814
+ }
815
+
816
+ return res.sendSuccess( { message: 'Expired approve status updated', updated: totalUpdated } );
817
+ } catch ( error ) {
818
+ logger.error( { error: error, message: req.body, function: 'expireApproveStatus' } );
819
+ return res.sendError( { error: error }, 500 );
820
+ }
821
+ }
822
+
139
823
  export async function revoptaggingcount( req, res ) {
140
824
  try {
141
825
  const openSearch = JSON.parse( process.env.OPENSEARCH );
@@ -211,44 +895,180 @@ export async function storeProcessedData( req, res ) {
211
895
  try {
212
896
  const openSearch = JSON.parse( process.env.OPENSEARCH );
213
897
  const inputData = req.query;
214
- const previousDate = dayjs( inputData.dateString ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
215
- const dateString = `${inputData.storeId}_${inputData.dateString}`;
216
- const dateStringPrevious = `${inputData.storeId}_${previousDate}`;
217
- const getQuery = {
218
- query: {
219
- terms: {
220
- _id: [ dateString, dateStringPrevious ],
221
- },
222
- },
223
- _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
224
- sort: [
225
- {
226
- date_iso: {
227
- order: 'desc',
898
+ const { fromDate, toDate, storeId } = inputData;
899
+
900
+ // Multi-date range handling for a single store
901
+ if ( fromDate && toDate && storeId ) {
902
+ const dayjs = ( await import( 'dayjs' ) ).default;
903
+ const isSameOrBefore = ( await import( 'dayjs/plugin/isSameOrBefore.js' ) ).default;
904
+ dayjs.extend( isSameOrBefore );
905
+
906
+ let start = dayjs( fromDate );
907
+ // get start value from the before day one
908
+ // Move start one day back so we can access "day before" when looping
909
+ start = start.subtract( 1, 'day' );
910
+ let end = dayjs( toDate );
911
+
912
+ if ( !start.isValid() || !end.isValid() ) {
913
+ return res.sendError( 'Invalid date range supplied', 400 );
914
+ }
915
+
916
+ if ( end.isBefore( start ) ) {
917
+ [ start, end ] = [ end, start ];
918
+ }
919
+
920
+ const allDateStrings = [];
921
+ const orderedDates = [];
922
+
923
+ while ( start.isSameOrBefore( end ) ) {
924
+ const formatted = start.format( 'YYYY-MM-DD' );
925
+ orderedDates.push( formatted );
926
+ allDateStrings.push( `${storeId}_${formatted}` );
927
+ start = start.add( 1, 'day' );
928
+ }
929
+
930
+ if ( allDateStrings.length === 0 ) {
931
+ return res.sendSuccess( [] );
932
+ }
933
+
934
+ const footfallQuery = {
935
+ query: {
936
+ terms: {
937
+ _id: allDateStrings,
228
938
  },
229
939
  },
230
- ],
231
- };
940
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
941
+ sort: [
942
+ { date_iso: { order: 'asc' } },
943
+ ],
944
+ size: allDateStrings.length,
945
+ };
232
946
 
233
- const getData = await getOpenSearchData( openSearch.footfall, getQuery );
234
- const hits = getData?.body?.hits?.hits || [];
947
+ const multiGet = await getOpenSearchData( openSearch.footfall, footfallQuery );
948
+ const multiHits = multiGet?.body?.hits?.hits || [];
949
+ const hitsMap = new Map();
950
+ multiHits.forEach( ( hit ) => {
951
+ hitsMap.set( hit?._id, hit?._source || null );
952
+ } );
235
953
 
236
- const processedData = hits.find( ( d ) => d._id === dateString )?._source || null;
237
- const previousData = hits.find( ( d ) => d._id === dateStringPrevious )?._source || null;
954
+ const responseArray = [];
238
955
 
239
- let footfallCountTrend = 0;
956
+ for ( let i = 1; i < orderedDates.length; i++ ) {
957
+ const currentDate = orderedDates[i];
958
+ const currentId = `${storeId}_${currentDate}`;
959
+ logger.info( { currentId, currentDate } );
960
+ const processedData = hitsMap.get( currentId );
961
+ logger.info( { processedData } );
962
+ if ( !processedData ) {
963
+ responseArray.push( {
964
+ date: currentDate,
965
+ footfallCount: 0,
966
+ footfallCountTrend: 0,
967
+ downtime: 0,
968
+ } );
969
+ continue;
970
+ }
240
971
 
241
- if ( processedData && previousData && previousData.footfall_count ) {
242
- footfallCountTrend = Math.round(
243
- ( ( processedData.footfall_count - previousData.footfall_count ) / previousData.footfall_count ) * 100,
244
- );
245
- }
972
+ const prevDate = dayjs( currentDate ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
973
+ const prevId = `${storeId}_${prevDate}`;
974
+ logger.info( { prevId, prevDate } );
975
+ const previousData = hitsMap.get( prevId );
246
976
 
247
- return res.sendSuccess( {
248
- footfallCount: processedData?.footfall_count || 0,
249
- footfallCountTrend,
250
- downtime: processedData?.down_time || 0,
251
- } );
977
+ let footfallCountTrend = 0;
978
+ logger.info( { previousData, previoucubr1: previousData?.footfall_count } );
979
+ if ( previousData && previousData.footfall_count ) {
980
+ logger.info( { previousData, previoucubr: previousData?.footfall_count } );
981
+ footfallCountTrend = Math.round(
982
+ ( ( processedData.footfall_count - previousData?.footfall_count ) / previousData.footfall_count ) * 100,
983
+ );
984
+ logger.info( { footfallCountTrend } );
985
+ }
986
+ // Add ticket status from openSearch.footfallDirectory (_source.status)
987
+ let ticketStatus = null;
988
+ let receivedfootfall = null;
989
+ // Try to find a matching footfallDirectory record for this date+storeId
990
+ // const ticketKey = `${storeId}_${currentDate}`;
991
+ const footfallDirQuery = {
992
+ query: {
993
+ bool: {
994
+ must: [
995
+ { term: { 'storeId.keyword': storeId } },
996
+ { term: { 'dateString': currentDate } },
997
+ { term: { 'ticketName.keyword': 'footfall-directory' } },
998
+ {
999
+ 'term': {
1000
+ 'type.keyword': 'store',
1001
+ },
1002
+ },
1003
+ ],
1004
+ },
1005
+ },
1006
+ size: 1,
1007
+ _source: [ 'status', 'mappingInfo', 'footfallCount' ],
1008
+ };
1009
+ try {
1010
+ const footfallDirRes = await getOpenSearchData( openSearch.footfallDirectory, footfallDirQuery );
1011
+ const hit = footfallDirRes?.body?.hits?.hits?.[0];
1012
+ receivedfootfall = hit._source.footfallCount;
1013
+ if ( hit?._source?.mappingInfo && Array.isArray( hit._source.mappingInfo ) ) {
1014
+ for ( let i = 0; i < hit._source.mappingInfo.length; i++ ) {
1015
+ if ( hit._source.mappingInfo[i].type === 'tagging' ) {
1016
+ ticketStatus = hit._source.mappingInfo[i].status;
1017
+ break;
1018
+ }
1019
+ }
1020
+ }
1021
+ } catch ( err ) {
1022
+ logger.warn( { message: 'Could not get ticket status from footfallDirectory', error: err } );
1023
+ }
1024
+ // Check if request status ("raised") should be enabled or disabled by querying MongoDB config
1025
+ // We'll assume findOnerevopConfig can fetch the record with status for this storeId and dateString
1026
+ let raisedStatusEnabled = 'reset'; // default: enabled
1027
+ try {
1028
+ const mongoConfig = await findOneVmsStoreRequest( { storeId: storeId, dateString: currentDate } );
1029
+ if ( mongoConfig && mongoConfig.status ) {
1030
+ raisedStatusEnabled = mongoConfig.status;
1031
+ }
1032
+ } catch ( err ) {
1033
+ logger.warn( { message: 'Could not get request status from MongoDB', error: err } );
1034
+ // Leave raisedStatusEnabled as default
1035
+ }
1036
+ let statusArray=[ 'Closed', 'Open - Accuracy Issue', 'Closed - Accuracy Issue' ];
1037
+
1038
+ responseArray.push( {
1039
+ date: processedData?.date_string || currentDate,
1040
+ footfallCount: processedData?.footfall_count || 0,
1041
+ footfallCountTrend,
1042
+ downtime: processedData?.down_time || 0,
1043
+ ticketStatus,
1044
+ raisedStatusEnabled,
1045
+ footfallticketCount: statusArray.includes( ticketStatus )?receivedfootfall:processedData?.footfall_count,
1046
+ } );
1047
+ console.log( '🚀 ~ storeProcessedData ~ ticketStatus:', ticketStatus );
1048
+
1049
+ if ( raisedStatusEnabled === 'block' ) {
1050
+ // Calculate the number of days from currentDate + 1 to the end of the month
1051
+ // Assume currentDate is in format 'YYYY-MM-DD'
1052
+ const currentDateObj = new Date( currentDate );
1053
+ // Move to next day
1054
+ const nextDay = new Date( currentDateObj );
1055
+ nextDay.setDate( currentDateObj.getDate() + 1 );
1056
+
1057
+ // Get the last date of the current month
1058
+ const endOfMonth = new Date( currentDateObj.getFullYear(), currentDateObj.getMonth() + 1, 0 );
1059
+
1060
+ // Calculate number of days (inclusive of end date, exclusive of nextDay)
1061
+ let noOfBlockedDays = Math.floor( ( endOfMonth - nextDay ) / ( 1000 * 60 * 60 * 24 ) ) + 1;
1062
+ if ( noOfBlockedDays < 0 ) noOfBlockedDays = 0;
1063
+
1064
+ // Add to response
1065
+ responseArray[responseArray.length - 1].noOfBlockedDays = noOfBlockedDays;
1066
+ }
1067
+ }
1068
+
1069
+ return res.sendSuccess( responseArray );
1070
+ }
1071
+ return res.sendError( 'Required parameters missing', 400 );
252
1072
  } catch ( error ) {
253
1073
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
254
1074
  const err = error.message || 'Internal Server Error';
@@ -284,43 +1104,183 @@ export async function footFallImages( req, res ) {
284
1104
  'ticketName.keyword': 'footfall-directory',
285
1105
  },
286
1106
  },
1107
+ {
1108
+ 'term': {
1109
+ 'type.keyword': 'store',
1110
+ },
1111
+ },
287
1112
  ],
288
1113
  },
289
1114
  },
290
- '_source': [ 'dateString', 'storeId', 'duplicateCount', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'junkCount', 'status', 'ticketId', 'comments', 'userName', 'role', 'createdAt', 'email', 'houseKeepingACCount', 'duplicateACCount', 'employeeACCount', 'junkACCount', 'approverEmail', 'approverRole', 'approverUserName' ],
1115
+ '_source': [ 'dateString', 'storeId', 'mappingInfo', 'revicedFootfall', 'revicedPerc', 'reviced', 'createdAt', 'updatedAt', 'footfallCount' ],
291
1116
 
292
1117
  };
293
1118
 
294
1119
  const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
295
1120
  const ticketDetails = getData?.body?.hits?.hits[0];
296
1121
  let temp = [];
297
- ticketDetails?._source? temp.push( ticketDetails?._source ) :null;
298
- // temp[0].status = 'open';
299
- if ( ticketDetails?._source?.status == 'closed' ) {
300
- delete temp[0].status;
301
- temp.push( { ...ticketDetails?._source, status: 'closed' } );
302
-
303
- temp[1].userName = getData?.body?.hits?.hits?.[0]?._source?.approverUserName;
304
- temp[1].email = getData?.body?.hits?.hits?.[0]?._source?.approverEmail;
305
- temp[1].role = getData?.body?.hits?.hits?.[0]?._source?.approverRole;
306
- temp[1].employeeCount = getData?.body?.hits?.hits?.[0]?._source?.employeeACCount;
307
- temp[1].houseKeepingCount = getData?.body?.hits?.hits?.[0]?._source?.houseKeepingACCount;
308
- temp[1].duplicateCount = getData?.body?.hits?.hits?.[0]?._source?.duplicateACCount;
309
- temp[1].junkCount = getData?.body?.hits?.hits?.[0]?._source?.junkACCount;
310
- }
311
- const LamdaURL = revop.getImages;
312
- let resultData = await LamdaServiceCall( LamdaURL, inputData );
313
- logger.info( { resultData: resultData } );
314
- if ( resultData ) {
315
- temp.length? temp[0].status = 'open': null;
316
- if ( resultData.status_code == '200' ) {
317
- return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0? temp : null, config: req?.store?.revopTagging } );
318
- } else {
319
- return res.sendError( 'No Content', 204 );
320
- }
321
- } else {
322
- return res.sendError( 'No Content', 204 );
323
- }
1122
+ const footfallValue = ticketDetails?._source?.footfallCount ?? 0;
1123
+ const mappingInfoArray = ticketDetails?._source?.mappingInfo ?? [];
1124
+ // Helper to get mappingInfo for an actionType
1125
+ function getMappingForType( type ) {
1126
+ return mappingInfoArray.find( ( m ) => m?.type === type ) || {};
1127
+ }
1128
+ const getTagging = getMappingForType( 'tagging' );
1129
+ const getFinal = getMappingForType( 'finalRevision' );
1130
+ const taggingRevised = getTagging.revicedFootfall ?? 0;
1131
+ const finalRevised = getFinal.revicedFootfall ?? 0;
1132
+ // List of actionTypes to process in sequence
1133
+
1134
+ if ( req.user.userType !== 'tango' && req.user.role !== 'superadmin' ) {
1135
+ switch ( req.user.role ) {
1136
+ case 'user':
1137
+ const actionTypesUser = [ 'tagging', 'finalRevision' ];
1138
+
1139
+ temp = [];
1140
+ actionTypesUser.forEach( ( type ) => {
1141
+ const mapping = getMappingForType( type );
1142
+ if ( type === 'tagging' ) {
1143
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
1144
+ const revisedPerc =
1145
+ footfallValue > 0 ?
1146
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
1147
+ '0';
1148
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1149
+ temp.push( {
1150
+ actionType: type,
1151
+ footfall: footfallValue,
1152
+ revicedFootfall: revisedFootfall,
1153
+ revicedPerc: revisedPerc,
1154
+ count: countObj,
1155
+ createdAt: mapping.createdAt ?? '',
1156
+ createdByEmail: mapping.createdByEmail ?? '',
1157
+ createdByUserName: mapping.createdByUserName ?? '',
1158
+ createdByRole: mapping.createdByRole ?? '',
1159
+ isUp: false,
1160
+ } );
1161
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
1162
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
1163
+ const revisedPerc =
1164
+ footfallValue > 0 ?
1165
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
1166
+ '0';
1167
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1168
+ temp.push( {
1169
+ actionType: type,
1170
+ footfall: footfallValue,
1171
+ revicedFootfall: revisedFootfall,
1172
+ revicedPerc: revisedPerc,
1173
+ count: countObj,
1174
+ createdAt: mapping.createdAt ?? '',
1175
+ createdByEmail: mapping.createdByEmail ?? '',
1176
+ createdByUserName: mapping.createdByUserName ?? '',
1177
+ createdByRole: mapping.createdByRole ?? '',
1178
+ isUp: finalRevised > taggingRevised? true : false,
1179
+ } );
1180
+ }
1181
+ } );
1182
+ break;
1183
+ case 'admin':
1184
+ const actionTypesAdmin = [ 'tagging', 'review', 'finalRevision' ];
1185
+ temp = [];
1186
+ actionTypesAdmin.forEach( ( type ) => {
1187
+ const mapping = getMappingForType( type );
1188
+ if ( type === 'tagging' ) {
1189
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
1190
+ const revisedPerc =
1191
+ footfallValue > 0 ?
1192
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
1193
+ '0';
1194
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1195
+ temp.push( {
1196
+ actionType: type,
1197
+ footfall: footfallValue,
1198
+ revicedFootfall: revisedFootfall,
1199
+ revicedPerc: revisedPerc,
1200
+ count: countObj,
1201
+ createdAt: mapping.createdAt ?? '',
1202
+ createdByEmail: mapping.createdByEmail ?? '',
1203
+ createdByUserName: mapping.createdByUserName ?? '',
1204
+ createdByRole: mapping.createdByRole ?? '',
1205
+ isUp: false,
1206
+ } );
1207
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
1208
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
1209
+ const revisedPerc =
1210
+ footfallValue > 0 ?
1211
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
1212
+ '0';
1213
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1214
+ temp.push( {
1215
+ actionType: type,
1216
+ footfall: footfallValue,
1217
+ revicedFootfall: revisedFootfall,
1218
+ revicedPerc: revisedPerc,
1219
+ count: countObj,
1220
+ createdAt: mapping.createdAt ?? '',
1221
+ createdByEmail: mapping.createdByEmail ?? '',
1222
+ createdByUserName: mapping.createdByUserName ?? '',
1223
+ createdByRole: mapping.createdByRole ?? '',
1224
+ isUp: finalRevised > taggingRevised? true : false,
1225
+ } );
1226
+ }
1227
+ } );
1228
+ }
1229
+ } else {
1230
+ const actionTypes = [ 'tagging', 'review', 'approve', 'tangoreview', 'finalRevision' ];
1231
+
1232
+ // Dynamically add to temp only if actionType matches and status is 'closed',
1233
+ // except for 'tagging' where status must be 'raised'
1234
+ temp = [];
1235
+ actionTypes.forEach( ( type ) => {
1236
+ const mapping = getMappingForType( type );
1237
+ if ( type === 'tagging' ) {
1238
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1239
+ temp.push( {
1240
+ actionType: type,
1241
+ footfall: footfallValue,
1242
+ revicedFootfall: mapping.revicedFootfall ?? 0,
1243
+ revicedPerc: mapping?.reviced?.toString() ?? '--',
1244
+ count: countObj,
1245
+ createdAt: mapping.createdAt ?? '',
1246
+ createdByEmail: mapping.createdByEmail ?? '',
1247
+ createdByUserName: mapping.createdByUserName ?? '',
1248
+ createdByRole: mapping.createdByRole ?? '',
1249
+ isUp: false,
1250
+ } );
1251
+ }
1252
+ if ( type !== 'tagging' && mapping.status === 'Closed' ) {
1253
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
1254
+ temp.push( {
1255
+ actionType: type,
1256
+ footfall: footfallValue,
1257
+ revicedFootfall: mapping.revicedFootfall ?? 0,
1258
+ revicedPerc: mapping?.reviced?.toString() ?? '--',
1259
+ count: countObj,
1260
+ createdAt: mapping.createdAt ?? '',
1261
+ createdByEmail: mapping.createdByEmail ?? '',
1262
+ createdByUserName: mapping.createdByUserName ?? '',
1263
+ createdByRole: mapping.createdByRole ?? '',
1264
+ isUp: finalRevised > taggingRevised? true : false,
1265
+ } );
1266
+ }
1267
+ } );
1268
+ }
1269
+
1270
+
1271
+ const LamdaURL = revop.getImages;
1272
+ let resultData = await LamdaServiceCall( LamdaURL, inputData );
1273
+ if ( resultData ) {
1274
+ // temp.length? temp[0].status = 'open': null;
1275
+
1276
+ if ( resultData.status_code == '200' ) {
1277
+ return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0 && ticketDetails? temp : null, config: req?.store?.footfallDirectoryConfigs } );
1278
+ } else {
1279
+ return res.sendError( 'No Content', 204 );
1280
+ }
1281
+ } else {
1282
+ return res.sendError( 'No Content', 204 );
1283
+ }
324
1284
  } catch ( error ) {
325
1285
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
326
1286
  const err = error.message || 'Internal Server Error';
@@ -330,12 +1290,20 @@ export async function footFallImages( req, res ) {
330
1290
 
331
1291
  export async function tagTempId( req, res ) {
332
1292
  try {
333
- const openSearch = JSON.parse( process.env.OPENSEARCH );
334
1293
  const inputData = req.body;
1294
+ const today = dayjs();
1295
+ const diff = today.diff( inputData.dateString, 'day' );
1296
+ const taggingDueDate = req?.client?.footfallDirectoryConfigs?.allowTicketCreation || 0;
1297
+ if ( diff > taggingDueDate ) {
1298
+ return res.sendError( `Tagging is not allowed for a period exceeding ${taggingDueDate} days`, 400 );
1299
+ }
1300
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1301
+
335
1302
  const upsertRecord = {
336
1303
  clientId: inputData.storeId.split( '-' )[0],
337
1304
  storeId: inputData.storeId,
338
1305
  tempId: inputData.tempId,
1306
+ id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
339
1307
  dateString: inputData.dateString,
340
1308
  timeRange: inputData.timeRange,
341
1309
  processType: inputData.processType,
@@ -344,10 +1312,31 @@ export async function tagTempId( req, res ) {
344
1312
  exitTime: inputData.exitTime,
345
1313
  filePath: inputData.filePath,
346
1314
  status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
347
- description: '',
348
- isChecked: inputData.isChecked,
349
- duplicateImage: inputData?.duplicateImage?.length>0? inputData?.duplicateImage :[],
1315
+ description: inputData.comments || '',
1316
+ isChecked: null,
1317
+ // Add id to each object in duplicateImage if it exists and is an array
1318
+ duplicateImage: Array.isArray( inputData?.duplicateImage ) ?
1319
+ inputData.duplicateImage.map( ( img ) => ( {
1320
+ ...img,
1321
+ id: `${inputData?.storeId}_${inputData?.dateString}_${img?.tempId}`,
1322
+ actions: [
1323
+ {
1324
+ actionType: 'tagging',
1325
+ action: 'submitted',
1326
+ },
1327
+ ],
1328
+ } ) ) :
1329
+ [],
350
1330
  type: 'tagging-reflect',
1331
+ ticketStatus: 'submitted',
1332
+ isParent: inputData?.revopsType === 'duplicate'? true : false,
1333
+ actions: [
1334
+ {
1335
+ actionType: 'tagging',
1336
+ action: 'submitted',
1337
+ },
1338
+ ],
1339
+ comments: inputData.comments || '',
351
1340
  createdAt: new Date(),
352
1341
  updatedAt: new Date(),
353
1342
 
@@ -363,8 +1352,10 @@ export async function tagTempId( req, res ) {
363
1352
  ctx._source.description = params.description;
364
1353
  ctx._source.isChecked = params.isChecked;
365
1354
  ctx._source.duplicateImage = params.duplicateImage;
1355
+ ctx._source.isParent = params.isParent;
366
1356
  ctx._source.updatedAt = params.updatedAt;
367
1357
  ctx._source.timeRange = params.timeRange;
1358
+ ctx._source.comments = params.comments;
368
1359
  if (ctx._source.createdAt == null) {
369
1360
  ctx._source.createdAt = params.createdAt;
370
1361
  ctx._source.clientId = params.clientId;
@@ -379,6 +1370,32 @@ export async function tagTempId( req, res ) {
379
1370
  };
380
1371
  const id = `${inputData.storeId}_${inputData.dateString}_${inputData.timeRange}_${inputData.tempId}`;
381
1372
  await upsertWithScript( openSearch.revop, id, { script, upsert: upsertRecord } );
1373
+ if ( inputData?.comments && inputData?.comments !== '' ) {
1374
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
1375
+ const logs = {
1376
+ type: 'tagging',
1377
+ parent: inputData?.revopsType == 'duplicate'? inputData.tempId : null,
1378
+ id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
1379
+ tempId: inputData.tempId,
1380
+ timeRange: inputData.timeRange,
1381
+ storeId: inputData.storeId,
1382
+ dateString: inputData.dateString,
1383
+ processType: inputData.processType,
1384
+ category: inputData.revopsType,
1385
+ entryTime: inputData.entryTime,
1386
+ exitTime: inputData.exitTime,
1387
+ filePath: inputData.filePath,
1388
+ status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
1389
+ description: inputData.comments || '',
1390
+ isChecked: null,
1391
+ createdByEmail: req?.user?.email,
1392
+ createdByUserName: req?.user?.userName,
1393
+ createdByRole: req?.user?.role,
1394
+ message: inputData.comments || '',
1395
+ createdAt: new Date(),
1396
+ };
1397
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
1398
+ }
382
1399
  if ( inputData?.duplicateImage?.length> 0 ) {
383
1400
  let bulkBody = [];
384
1401
  for ( let item of inputData?.duplicateImage ) {
@@ -386,9 +1403,10 @@ export async function tagTempId( req, res ) {
386
1403
  clientId: inputData.storeId.split( '-' )[0],
387
1404
  storeId: inputData.storeId,
388
1405
  tempId: item.tempId,
1406
+ id: `${inputData?.storeId}_${inputData?.dateString}_${item?.tempId}`,
389
1407
  dateString: inputData.dateString,
390
1408
  timeRange: item.timeRange,
391
- isChecked: item.isChecked,
1409
+ isChecked: null,
392
1410
  processType: inputData.processType,
393
1411
  revopsType: item.revopsType,
394
1412
  entryTime: item.entryTime,
@@ -397,7 +1415,16 @@ export async function tagTempId( req, res ) {
397
1415
  status: item?.revopsType == 'non-tagging' ?'':'submitted',
398
1416
  description: '',
399
1417
  duplicateImage: [],
1418
+ isParent: false,
400
1419
  type: 'tagging-reflect',
1420
+ ticketStatus: 'submitted',
1421
+ comments: inputData.comments || '',
1422
+ actions: [
1423
+ {
1424
+ actionType: 'tagging',
1425
+ action: 'submitted',
1426
+ },
1427
+ ],
401
1428
  createdAt: new Date(),
402
1429
  updatedAt: new Date(),
403
1430
 
@@ -419,12 +1446,20 @@ export async function tagTempId( req, res ) {
419
1446
  return { success: false, errors: res1.items };
420
1447
  } else {
421
1448
  logger.info( { msg: 'res1' } );
422
- return res.sendSuccess( `ID tagged as duplicates` );
1449
+ return res.sendSuccess( `ID tagged as Duplicates` );
423
1450
  // return { success: true };
424
1451
  }
425
1452
  }
426
1453
  } else {
427
- const message = inputData?.revopsType == 'non-tagging' ? 'ID removed from tagging' :inputData?.revopsType == 'employee' ?'ID tagged as an employee/staff':inputData?.revopsType == 'junk'? 'ID tagged as a junk':'ID tagged as an house keeping';
1454
+ // Convert camelCase revopsType to space-separated and capitalize first letter of each word
1455
+ function camelCaseToTitle( str ) {
1456
+ if ( !str ) return '';
1457
+ return str.replace( /([A-Z])/g, ' $1' ).replace( /^./, ( s ) => s.toUpperCase() );
1458
+ }
1459
+ const titleRevopsType = camelCaseToTitle( inputData?.revopsType );
1460
+ const message = inputData?.revopsType == 'non-tagging' ?
1461
+ 'ID removed from tagging' :
1462
+ `ID tagged as ${titleRevopsType}`;
428
1463
  return res.sendSuccess( message );
429
1464
  }
430
1465
  } catch ( error ) {
@@ -451,3 +1486,390 @@ export async function getCategorizedImages( req, res ) {
451
1486
  return res.sendError( err, 500 );
452
1487
  }
453
1488
  }
1489
+
1490
+ export async function vmsDataMigration( req, res ) {
1491
+ try {
1492
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1493
+ const inputData = req.body;
1494
+ const { storeId, dateString, limit = 10000 } = inputData;
1495
+
1496
+ // Build query to fetch old structure documents
1497
+ const query = {
1498
+ query: {
1499
+ bool: {
1500
+ must: [
1501
+ {
1502
+ term: {
1503
+ 'ticketName.keyword': 'footfall-directory',
1504
+ },
1505
+ },
1506
+ // {
1507
+ // range: {
1508
+ // createdAt: {
1509
+ // gte: '2025-12-01T00:00:00.000Z',
1510
+ // lte: '2025-12-03T00:00:00.000Z',
1511
+ // },
1512
+ // },
1513
+ // },
1514
+ ],
1515
+ },
1516
+ },
1517
+ size: parseInt( limit ),
1518
+ };
1519
+
1520
+ // Add storeId filter if provided
1521
+ if ( storeId ) {
1522
+ query.query.bool.must.push( {
1523
+ term: {
1524
+ 'storeId.keyword': storeId,
1525
+ },
1526
+ } );
1527
+ }
1528
+
1529
+ // Add dateString filter if provided
1530
+ if ( dateString ) {
1531
+ query.query.bool.must.push( {
1532
+ terms: {
1533
+ dateString: dateString?.split( ',' ),
1534
+ },
1535
+ } );
1536
+ }
1537
+
1538
+ // Exclude documents that already have the new structure (have mappingInfo or type: 'store')
1539
+ query.query.bool.must_not = [
1540
+ {
1541
+ exists: {
1542
+ field: 'mappingInfo',
1543
+ },
1544
+ },
1545
+ {
1546
+ term: {
1547
+ 'type.keyword': 'store',
1548
+ },
1549
+ },
1550
+ ];
1551
+
1552
+ const getData = await getOpenSearchData( openSearch.oldFootfallDirectory, query );
1553
+ logger.info( { getData } );
1554
+ const hits = getData?.body?.hits?.hits || [];
1555
+
1556
+ if ( hits.length === 0 ) {
1557
+ return res.sendSuccess( { message: 'No documents found to migrate', migrated: 0 } );
1558
+ }
1559
+
1560
+ let migratedCount = 0;
1561
+ const errors = [];
1562
+
1563
+ for ( const hit of hits ) {
1564
+ try {
1565
+ const oldSource = hit._source;
1566
+ const documentId = hit._id;
1567
+
1568
+ // Calculate revicedFootfall (sum of AC counts)
1569
+ const tempFootfall =oldSource?.status === 'open' ?
1570
+ ( oldSource.duplicateCount || 0 ) +
1571
+ ( oldSource.employeeCount || 0 ) +
1572
+ ( oldSource.houseKeepingCount || 0 ) +
1573
+ ( oldSource.junkCount || 0 ):
1574
+ ( oldSource.duplicateACCount || 0 ) +
1575
+ ( oldSource.employeeACCount || 0 ) +
1576
+ ( oldSource.houseKeepingACCount || 0 ) +
1577
+ ( oldSource.junkACCount || 0 );
1578
+
1579
+ // Calculate revicedPerc
1580
+ const footfallCount = oldSource.footfallCount || 0;
1581
+ const revicedFootfall = footfallCount - tempFootfall;
1582
+ const revicedPerc = footfallCount > 0 ?
1583
+ Math.round( ( revicedFootfall / footfallCount ) * 100 ) :
1584
+ 0;
1585
+ // Calculate reviced
1586
+ const reviced = parseInt( revicedPerc );
1587
+
1588
+ // Transform arrays to revisedDetail format
1589
+ const revisedDetail = [];
1590
+
1591
+ // Transform duplicateImages
1592
+ if ( Array.isArray( oldSource.duplicateImages ) ) {
1593
+ for ( const duplicate of oldSource.duplicateImages ) {
1594
+ const parentId = `${oldSource.storeId}_${oldSource.dateString}_${duplicate.tempId}`;
1595
+ const parentDetail = {
1596
+ id: parentId,
1597
+ clientId: oldSource.clientId,
1598
+ storeId: oldSource.storeId,
1599
+ tempId: duplicate.tempId,
1600
+ dateString: oldSource.dateString,
1601
+ timeRange: duplicate.timeRange,
1602
+ processType: 'footfall',
1603
+ revopsType: 'duplicate',
1604
+ entryTime: duplicate.entryTime,
1605
+ exitTime: duplicate.exitTime,
1606
+ filePath: duplicate.filePath,
1607
+ status: oldSource.duplicateStatus || 'submitted',
1608
+ description: '',
1609
+ isChecked: duplicate.isChecked !== undefined ? duplicate.isChecked : false,
1610
+ type: 'tagging-reflect',
1611
+ parent: null,
1612
+ isParent: true,
1613
+ createdAt: oldSource.createdAt || new Date(),
1614
+ updatedAt: oldSource.updatedAt || new Date(),
1615
+ duplicateImage: [],
1616
+ };
1617
+
1618
+ // Add child duplicate images
1619
+ if ( Array.isArray( duplicate.data ) ) {
1620
+ parentDetail.duplicateImage = duplicate.data.map( ( child ) => ( {
1621
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1622
+ tempId: child.tempId,
1623
+ timeRange: child.timeRange,
1624
+ entryTime: child.entryTime,
1625
+ exitTime: child.exitTime,
1626
+ filePath: child.filePath,
1627
+ isChecked: child.isChecked !== undefined ? child.isChecked : true,
1628
+ } ) );
1629
+
1630
+ // Add child details to revisedDetail
1631
+ for ( const child of duplicate.data ) {
1632
+ revisedDetail.push( {
1633
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1634
+ clientId: oldSource.clientId,
1635
+ storeId: oldSource.storeId,
1636
+ tempId: child.tempId,
1637
+ dateString: oldSource.dateString,
1638
+ timeRange: child.timeRange,
1639
+ processType: 'footfall',
1640
+ revopsType: 'duplicate',
1641
+ entryTime: child.entryTime,
1642
+ exitTime: child.exitTime,
1643
+ filePath: child.filePath,
1644
+ status: oldSource.duplicateStatus || 'submitted',
1645
+ description: '',
1646
+ isChecked: child.isChecked !== undefined ? child.isChecked : false,
1647
+ type: 'tagging-reflect',
1648
+ parent: duplicate.tempId,
1649
+ isParent: false,
1650
+ createdAt: oldSource.createdAt || new Date(),
1651
+ updatedAt: oldSource.updatedAt || new Date(),
1652
+ duplicateImage: [],
1653
+ } );
1654
+ }
1655
+ }
1656
+
1657
+ revisedDetail.push( parentDetail );
1658
+ }
1659
+ }
1660
+
1661
+ // Transform houseKeeping
1662
+ if ( Array.isArray( oldSource.houseKeeping ) ) {
1663
+ for ( const item of oldSource.houseKeeping ) {
1664
+ revisedDetail.push( {
1665
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1666
+ clientId: oldSource.clientId,
1667
+ storeId: oldSource.storeId,
1668
+ tempId: item.tempId,
1669
+ dateString: oldSource.dateString,
1670
+ timeRange: item.timeRange,
1671
+ processType: 'footfall',
1672
+ revopsType: 'houseKeeping',
1673
+ entryTime: item.entryTime,
1674
+ exitTime: item.exitTime,
1675
+ filePath: item.filePath,
1676
+ status: oldSource.houseKeepingStatus || 'submitted',
1677
+ description: '',
1678
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1679
+ type: 'tagging-reflect',
1680
+ parent: null,
1681
+ isParent: false,
1682
+ createdAt: oldSource.createdAt || new Date(),
1683
+ updatedAt: oldSource.updatedAt || new Date(),
1684
+ duplicateImage: [],
1685
+ } );
1686
+ }
1687
+ }
1688
+
1689
+ // Transform employee
1690
+ if ( Array.isArray( oldSource.employee ) ) {
1691
+ for ( const item of oldSource.employee ) {
1692
+ revisedDetail.push( {
1693
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1694
+ clientId: oldSource.clientId,
1695
+ storeId: oldSource.storeId,
1696
+ tempId: item.tempId,
1697
+ dateString: oldSource.dateString,
1698
+ timeRange: item.timeRange,
1699
+ processType: 'footfall',
1700
+ revopsType: 'employee',
1701
+ entryTime: item.entryTime,
1702
+ exitTime: item.exitTime,
1703
+ filePath: item.filePath,
1704
+ status: oldSource.employeeStatus || 'submitted',
1705
+ description: '',
1706
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1707
+ type: 'tagging-reflect',
1708
+ parent: null,
1709
+ isParent: false,
1710
+ createdAt: oldSource.createdAt || new Date(),
1711
+ updatedAt: oldSource.updatedAt || new Date(),
1712
+ duplicateImage: [],
1713
+ } );
1714
+ }
1715
+ }
1716
+
1717
+ // Transform junk
1718
+ if ( Array.isArray( oldSource.junk ) ) {
1719
+ for ( const item of oldSource.junk ) {
1720
+ revisedDetail.push( {
1721
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1722
+ clientId: oldSource.clientId,
1723
+ storeId: oldSource.storeId,
1724
+ tempId: item.tempId,
1725
+ dateString: oldSource.dateString,
1726
+ timeRange: item.timeRange,
1727
+ processType: 'footfall',
1728
+ revopsType: 'junk',
1729
+ entryTime: item.entryTime,
1730
+ exitTime: item.exitTime,
1731
+ filePath: item.filePath,
1732
+ status: 'submitted',
1733
+ description: '',
1734
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1735
+ type: 'tagging-reflect',
1736
+ parent: null,
1737
+ isParent: false,
1738
+ createdAt: oldSource.createdAt || new Date(),
1739
+ updatedAt: oldSource.updatedAt || new Date(),
1740
+ duplicateImage: [],
1741
+ } );
1742
+ }
1743
+ }
1744
+
1745
+ // Create count array
1746
+ const count = [
1747
+ {
1748
+ name: 'Duplicate',
1749
+ value: oldSource.duplicateCount || 0,
1750
+ key: 'duplicateCount',
1751
+ type: 'duplicate',
1752
+ },
1753
+ {
1754
+ name: 'Employee',
1755
+ value: oldSource.employeeCount || 0,
1756
+ key: 'employeeCount',
1757
+ type: 'employee',
1758
+ },
1759
+ {
1760
+ name: 'House keeping',
1761
+ value: oldSource.houseKeepingCount || 0,
1762
+ key: 'houseKeepingCount',
1763
+ type: 'houseKeeping',
1764
+ },
1765
+ {
1766
+ name: 'Junk',
1767
+ value: oldSource.junkCount || 0,
1768
+ key: 'junkCount',
1769
+ type: 'junk',
1770
+ },
1771
+ ];
1772
+
1773
+ // Create mappingInfo array
1774
+ const mappingInfo = oldSource.status === 'open' ?
1775
+ [
1776
+ {
1777
+ type: 'tagging',
1778
+ mode: 'mobile',
1779
+ revicedFootfall,
1780
+ revicedPerc: `${revicedPerc}%`,
1781
+ reviced,
1782
+ count,
1783
+ revisedDetail,
1784
+ status: oldSource.status === 'open' ? 'Raised' : oldSource.status || 'Raised',
1785
+ createdByEmail: oldSource.email || '',
1786
+ createdByUserName: oldSource.userName || '',
1787
+ createdByRole: oldSource.role || 'user',
1788
+ createdAt: oldSource.createdAt || new Date(),
1789
+ },
1790
+ {
1791
+ type: 'review',
1792
+ count,
1793
+ revisedDetail,
1794
+ status: oldSource.status === 'open' ? 'Open' : oldSource.status || 'Open',
1795
+ dueDate: oldSource.updatedAt ? new Date( new Date( oldSource.updatedAt ).getTime() + 3 * 24 * 60 * 60 * 1000 ) : new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ),
1796
+ createdAt: oldSource.createdAt || new Date(),
1797
+ },
1798
+ ]:
1799
+
1800
+ [
1801
+ {
1802
+ type: 'tagging',
1803
+ mode: 'web',
1804
+ revicedFootfall,
1805
+ revicedPerc: `${revicedPerc}%`,
1806
+ reviced,
1807
+ count,
1808
+ revisedDetail,
1809
+ status: oldSource.status === 'closed' ? 'Closed' : oldSource.status || 'Closed',
1810
+ createdByEmail: oldSource.email || '',
1811
+ createdByUserName: oldSource.userName || '',
1812
+ createdByRole: oldSource.role || 'user',
1813
+ createdAt: oldSource.createdAt || new Date(),
1814
+ },
1815
+ {
1816
+ type: 'review',
1817
+ mode: 'web',
1818
+ revicedFootfall,
1819
+ revicedPerc: `${revicedPerc}%`,
1820
+ reviced,
1821
+ count,
1822
+ revisedDetail,
1823
+ status: oldSource.status === 'closed' ? 'Closed' : oldSource.status || 'Closed',
1824
+ dueDate: oldSource.createdAt ? new Date( new Date( oldSource.createdAt ).getTime() + 3 * 24 * 60 * 60 * 1000 ) : new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ),
1825
+ createdAt: oldSource.createdAt || new Date(),
1826
+ createdByEmail: oldSource.approverEmail || '',
1827
+ createdByUserName: oldSource.approverUserName || '',
1828
+ createdByRole: oldSource.approverRole || 'user',
1829
+ },
1830
+ ];
1831
+ // Create new structure
1832
+ const newSource = {
1833
+ storeId: oldSource.storeId,
1834
+ type: 'store',
1835
+ dateString: oldSource.dateString,
1836
+ storeName: oldSource.storeName,
1837
+ ticketName: oldSource.ticketName,
1838
+ footfallCount: oldSource.footfallCount,
1839
+ clientId: oldSource.clientId,
1840
+ ticketId: oldSource.ticketId,
1841
+ createdAt: oldSource.createdAt,
1842
+ updatedAt: oldSource.updatedAt,
1843
+ status: oldSource.status === 'open' ? 'Raised' : ( oldSource.status === 'closed' ? 'Closed' : 'Raised' ),
1844
+ comments: oldSource.comments || '',
1845
+ revicedFootfall,
1846
+ revicedPerc: `${revicedPerc}%`,
1847
+ reviced,
1848
+ mappingInfo,
1849
+ };
1850
+
1851
+ // Update document in OpenSearch
1852
+ const updatedData = await updateOpenSearchData( openSearch.footfallDirectory, documentId, { doc: newSource, doc_as_upsert: true } );
1853
+ logger.info( { updatedData } );
1854
+ migratedCount++;
1855
+
1856
+ logger.info( { message: 'Document migrated successfully', newSource, documentId, storeId: oldSource.storeId, dateString: oldSource.dateString } );
1857
+ } catch ( error ) {
1858
+ const errorMsg = `Error migrating document ${hit._id}: ${error.message}`;
1859
+ errors.push( errorMsg );
1860
+ logger.error( { error: error, documentId: hit._id, function: 'vmsDataMigration' } );
1861
+ }
1862
+ }
1863
+
1864
+ return res.sendSuccess( {
1865
+ message: `Migration completed. ${migratedCount} document(s) migrated.`,
1866
+ migrated: migratedCount,
1867
+ total: hits.length,
1868
+ errors: errors.length > 0 ? errors : undefined,
1869
+ } );
1870
+ } catch ( error ) {
1871
+ logger.error( { error: error, message: req.query, function: 'vmsDataMigration' } );
1872
+ const err = error.message || 'Internal Server Error';
1873
+ return res.sendError( err, 500 );
1874
+ }
1875
+ }