tango-app-api-infra 3.9.5-vms.9 → 3.9.6

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,11 +1,11 @@
1
1
  {
2
2
  "name": "tango-app-api-infra",
3
- "version": "3.9.5-vms.9",
3
+ "version": "3.9.6",
4
4
  "description": "infra",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "start": "nodemon --exec \"eslint --fix . && node app.js\""
8
+ "start": "nodemon --exec \"eslint --fix . && node index.js\""
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18.10.0"
@@ -27,7 +27,7 @@
27
27
  "mongodb": "^6.4.0",
28
28
  "nodemon": "^3.1.0",
29
29
  "swagger-ui-express": "^5.0.0",
30
- "tango-api-schema": "^2.4.28",
30
+ "tango-api-schema": "^2.3.8",
31
31
  "tango-app-api-middleware": "^3.1.93",
32
32
  "winston": "^3.12.0",
33
33
  "winston-daily-rotate-file": "^5.0.0"
@@ -1,12 +1,11 @@
1
1
  import { chunkArray, download, logger, sendMessageToFIFOQueue, sendMessageToQueue } from 'tango-app-api-middleware';
2
- import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData, upsertOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
2
+ import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
3
3
  import { findOneStore } from '../services/store.service.js';
4
4
  import { countDocumnetsCamera } from '../services/camera.service.js';
5
5
  import { findOneRevopDownload, upsertRevopDownload } from '../services/revopDownload.service.js';
6
6
  import dayjs from 'dayjs';
7
7
  import utc from 'dayjs/plugin/utc.js';
8
8
  import timezone from 'dayjs/plugin/timezone.js';
9
- import { findUser } from '../services/user.service.js';
10
9
 
11
10
  dayjs.extend( utc );
12
11
  dayjs.extend( timezone );
@@ -134,7 +133,7 @@ async function bulkUpdateStatusToPending( indexName, inputData ) {
134
133
  }
135
134
  }
136
135
 
137
- export async function ticketSummary1( req, res ) {
136
+ export async function ticketSummary( req, res ) {
138
137
  try {
139
138
  const openSearch = JSON.parse( process.env.OPENSEARCH );
140
139
  const inputData = req.query;
@@ -216,59 +215,7 @@ export async function ticketSummary1( req, res ) {
216
215
  }
217
216
  }
218
217
 
219
- export async function ticketSummary( req, res ) {
220
- try {
221
- let result = '';
222
- const userInfo = req.user;
223
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
224
-
225
- if ( req.user.userType =='tango' ) {
226
- result ={
227
- totalTickets: 0,
228
- averageAccuracyOverAll: 0,
229
- openTickets: 0,
230
- openInfraIssues: 0,
231
- inprogress: 0,
232
- closedTickets: 0,
233
- ticketAccuracyAbove: '0%',
234
- ticketAccuracyBelow: '0%',
235
- };
236
- } else {
237
- result = req.user.role === 'superadmin'?
238
- {
239
- totalTickets: 0,
240
- openTickets: 0,
241
- inprogress: 0,
242
- closedTickets: 0,
243
- dueToday: 0,
244
- Expired: 0,
245
- underTangoReview: 0,
246
- avgTicket: '0%',
247
- avgAccuracy: '0%',
248
- } :
249
- req.user.role === 'user'? 'NA':
250
- ticketsFeature?
251
- {
252
- totalTickets: 0,
253
- openTickets: 0,
254
- inprogress: 0,
255
- closedTickets: 0,
256
- dueToday: 0,
257
- Expired: 0,
258
- avgTicket: '0%',
259
- avgAccuracy: '0%',
260
- }: 'NA';
261
- }
262
-
263
- return res.sendSuccess( { result: result } );
264
- } catch ( error ) {
265
- const err = error.message || 'Internal Server Error';
266
- logger.error( { error: error, messgage: req.query } );
267
- return res.sendSuccess( err, 500 );
268
- }
269
- }
270
-
271
- export async function ticketList1( req, res ) {
218
+ export async function ticketList( req, res ) {
272
219
  try {
273
220
  const openSearch = JSON.parse( process.env.OPENSEARCH );
274
221
  const inputData = req.query;
@@ -489,162 +436,6 @@ export async function ticketList1( req, res ) {
489
436
  }
490
437
  }
491
438
 
492
- export async function ticketList( req, res ) {
493
- try {
494
- const openSearch = JSON.parse( process.env.OPENSEARCH );
495
- const inputData= req.query;
496
- const userInfo = req.user;
497
- const limit =inputData?.limit || 10;
498
- const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
499
-
500
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
501
-
502
- // Get ticket list from the footfallDirectory index in OpenSearch
503
- // Assumes you have openSearch and getOpenSearchData available in this scope.
504
- // `openSearch.footfallDirectory` is the index for the tickets.
505
- // Uses req.query for any filter (add additional filtering logic per your requirements).
506
-
507
- const searchQuery = {
508
-
509
- size: limit, // or use parseInt(req.query.limit) for dynamic
510
- from: offset, // or use parseInt(req.query.offset) for dynamic
511
- sort: [ { 'createdAt': { order: 'desc' } } ],
512
- query: {
513
- bool: {
514
- must: [],
515
- },
516
- },
517
- };
518
-
519
- // Example: Filtering by storeId if present in the query
520
- if ( inputData.storeId ) {
521
- searchQuery.query.bool.must.push( {
522
- term: { 'storeId.keyword': inputData.storeId },
523
- } );
524
- }
525
- // Example: Filtering by status if provided
526
- if ( inputData.status ) {
527
- searchQuery.query.bool.must.push( {
528
- term: { 'status.keyword': inputData.status },
529
- } );
530
- }
531
-
532
- // You can add more filters as needed
533
- logger.info( { searchQuery, index: openSearch.footfallDirectory } );
534
- const searchResult = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
535
-
536
-
537
- const count = searchResult?.body?.hits?.total?.value || 0;
538
- if ( count === 0 ) {
539
- return res.sendError( 'no data found', 204 );
540
- }
541
- const ticketListData = searchResult?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
542
-
543
- let temp =[];
544
- if ( req.user.userType =='tango' ) {
545
- if ( inputData.tangotype == 'store' ) {
546
- for ( let item of ticketListData ) {
547
- temp.push( {
548
-
549
- ticketId: item?.ticketId,
550
- storeId: item?.storeId,
551
- storeName: item?.storeName,
552
-
553
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
554
- issueDate: item?.dateString,
555
- dueDate: '',
556
- footfall: item?.footfall,
557
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
558
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
559
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
560
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
561
- status: item?.status,
562
-
563
- } );
564
- }
565
- } else {
566
- for ( let item of ticketListData ) {
567
- temp.push( {
568
-
569
- ticketId: item?.ticketId,
570
- storeId: item?.storeId,
571
- storeName: item?.storeName,
572
-
573
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
574
- issueDate: item?.dateString,
575
- footfall: item?.footfall,
576
- dueDate: '',
577
- type: item.type || 'store',
578
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
579
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
580
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
581
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
582
- status: item?.status,
583
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
584
-
585
- } );
586
- }
587
- }
588
- } else {
589
- if ( req.user.role === 'superadmin' ) {
590
- for ( let item of ticketListData ) {
591
- temp.push( {
592
-
593
- ticketId: item?.ticketId,
594
- storeId: item?.storeId,
595
- storeName: item?.storeName,
596
-
597
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
598
- issueDate: item?.dateString,
599
- dueDate: '',
600
- footfall: item?.footfall,
601
-
602
- type: item.type || 'store',
603
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
604
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
605
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
606
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
607
- status: item?.status,
608
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
609
- approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail||'--',
610
-
611
- } );
612
- }
613
- } else if ( req.user.role === 'user' ) {
614
- temp = [];
615
- } else if ( ticketsFeature ) {
616
- for ( let item of ticketListData ) {
617
- temp.push( {
618
-
619
- ticketId: item?.ticketId,
620
- storeId: item?.storeId,
621
- storeName: item?.storeName,
622
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
623
- issueDate: item?.dateString,
624
- footfall: item?.footfall,
625
- dueDate: '',
626
- type: item.type || 'store',
627
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
628
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
629
-
630
- status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status|| '--',
631
- ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail||'--',
632
-
633
- } );
634
- }
635
- } else {
636
- temp =[];
637
- }
638
- }
639
-
640
- return res.sendSuccess( { result: temp } );
641
- } catch ( error ) {
642
- const err = error.message || 'Internal Server Error';
643
- logger.error( { error: error, messgage: req.query } );
644
- return res.sendSuccess( err, 500 );
645
- }
646
- }
647
-
648
439
  export async function getTickets( req, res ) {
649
440
  try {
650
441
  const openSearch = JSON.parse( process.env.OPENSEARCH );
@@ -653,7 +444,7 @@ export async function getTickets( req, res ) {
653
444
  const skip = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
654
445
  inputData.storeId = inputData.storeId.split( ',' ); // convert strig to array
655
446
  logger.info( { inputData: inputData, limit: limit, skip: skip } );
656
- let source = [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail' ];
447
+ let source = [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail' ];
657
448
  let filter = [
658
449
 
659
450
  {
@@ -729,10 +520,10 @@ export async function getTickets( req, res ) {
729
520
  },
730
521
  },
731
522
  } );
732
- source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'email', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
733
- inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
734
- inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
735
- inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkACCount', 'junkStatus', 'approverRole', 'approverUserName', 'approverEmail' ] : [];
523
+ source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'email', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
524
+ inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
525
+ inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
526
+ inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkACCount', 'junkStatus', 'approverRole', 'approverUserName', 'approverEmail' ] : [];
736
527
  }
737
528
 
738
529
  if ( inputData.action ) {
@@ -841,10 +632,7 @@ export async function getTickets( req, res ) {
841
632
  storeId: hit._source.storeId,
842
633
  dateString: hit?._source?.dateString,
843
634
  ticketName: hit?._source?.ticketName,
844
- status: hit?._source?.status?.revicedFootfall,
845
- revicedPerc: hit?._source?.revicedPerc,
846
- revicedFootfall: hit?._source?.revicedFootfall,
847
- mappingInfo: hit?._source?.mappingInfo,
635
+ status: hit?._source?.status,
848
636
  employeeStatus: hit?._source?.employeeStatus,
849
637
  houseKeepingStatus: hit?._source?.houseKeepingStatus,
850
638
  duplicateStatus: hit?._source?.duplicateStatus,
@@ -1716,305 +1504,3 @@ async function extractTempIds( document ) {
1716
1504
  return result;
1717
1505
  }
1718
1506
 
1719
- export async function reviewerList( req, res ) {
1720
- try {
1721
- const inputData = req.query;
1722
- // Build the query for users who have rolespermission with featureName "FootfallDirectory",
1723
- // and a module "Reviewer" where isAdd or isEdit is true.
1724
- const reviewerRoleQuery = {
1725
- 'clientId': inputData.clientId,
1726
- 'rolespermission': {
1727
- $elemMatch: {
1728
- featureName: 'FootfallDirectory',
1729
- modules: {
1730
- $elemMatch: {
1731
- name: 'Reviewer',
1732
- $or: [ { isAdd: true }, { isEdit: true } ],
1733
- },
1734
- },
1735
- },
1736
- },
1737
- };
1738
-
1739
- const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
1740
- return res.sendSuccess( getUserlist|| [] );
1741
- } catch ( error ) {
1742
- const err = error.message || 'Internal Server Error';
1743
- return res.sendError( err, 500 );
1744
- }
1745
- }
1746
-
1747
- export async function openTicketList( req, res ) {
1748
- try {
1749
- const inputData = req.body;
1750
- const openSearch = JSON.parse( process.env.OPENSEARCH );
1751
-
1752
- // INSERT_YOUR_CODE
1753
- // Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
1754
- const { clientId, fromDate, toDate } = inputData;
1755
-
1756
- const filter = [
1757
- {
1758
- terms: {
1759
- clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
1760
- },
1761
- },
1762
- {
1763
- range: {
1764
- dateString: {
1765
- gte: fromDate,
1766
- lte: toDate,
1767
- format: 'yyyy-MM-dd',
1768
- },
1769
- },
1770
- },
1771
- ];
1772
-
1773
- const openSearchQuery = {
1774
- size: 10000,
1775
- query: {
1776
- bool: {
1777
- filter: filter,
1778
- },
1779
- },
1780
- _source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
1781
- };
1782
-
1783
-
1784
- // Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
1785
- const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
1786
- const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1787
- return res.sendSuccess( getUserlist|| [] );
1788
- } catch ( error ) {
1789
- const err = error.message || 'Internal Server Error';
1790
- logger.error( { error: error, function: 'openTicketList' } );
1791
- return res.sendError( err, 500 );
1792
- }
1793
- }
1794
-
1795
- export async function assignTicket( req, res ) {
1796
- try {
1797
- const inputData = req.body;
1798
- const openSearch = JSON.parse( process.env.OPENSEARCH );
1799
-
1800
- // INSERT_YOUR_CODE
1801
- // Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
1802
- const { email, userName, role, actionType } = inputData;
1803
-
1804
- // INSERT_YOUR_CODE
1805
-
1806
- // Find and update mappingInfo fields for the provided ticketId and actionType
1807
- // Requires ticketId in inputData
1808
- const { ticketId } = inputData;
1809
- if ( !ticketId ) {
1810
- return res.sendError( 'ticketId is required', 400 );
1811
- }
1812
-
1813
- // Build the OpenSearch update-by-query body
1814
- const updateBody = {
1815
- script: {
1816
- source: `
1817
- if (ctx._source.mappingInfo != null) {
1818
- for (int i = 0; i < ctx._source.mappingInfo.length; i++) {
1819
- if (ctx._source.mappingInfo[i].type == params.actionType) {
1820
- ctx._source.mappingInfo[i].createdByEmail = params.email;
1821
- ctx._source.mappingInfo[i].createdByUserName = params.userName;
1822
- ctx._source.mappingInfo[i].createdByRole = params.role;
1823
- }
1824
- }
1825
- }
1826
- `,
1827
- lang: 'painless',
1828
- params: {
1829
- email,
1830
- userName,
1831
- role,
1832
- actionType,
1833
- },
1834
- },
1835
- query: {
1836
- bool: {
1837
- must: [
1838
- { term: { 'ticketId.keyword': ticketId } },
1839
- {
1840
- nested: {
1841
- path: 'mappingInfo',
1842
- query: {
1843
- bool: {
1844
- must: [
1845
- { match: { 'mappingInfo.type': actionType } },
1846
- ],
1847
- },
1848
- },
1849
- },
1850
- },
1851
- ],
1852
- },
1853
- },
1854
- };
1855
-
1856
- // Call OpenSearch _update_by_query to update doc(s) where ticketId and mappingInfo[i].type == actionType
1857
- const response = await upsertOpenSearchData(
1858
- openSearch.footfallDirectory,
1859
- '11-1716_2025-11-20_footfall-directory-tagging',
1860
- updateBody, // custom arg to indicate passthrough for update-by-query, depends on helper implementation
1861
- );
1862
-
1863
-
1864
- logger.info( { response } );
1865
-
1866
- return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
1867
- } catch ( error ) {
1868
- const err = error.message || 'Internal Server Error';
1869
- logger.error( { error: error, function: 'assignTicket' } );
1870
- return res.sendError( err, 500 );
1871
- }
1872
- }
1873
-
1874
- export async function updateTempStatus( req, res ) {
1875
- try {
1876
- const openSearch = JSON.parse( process.env.OPENSEARCH );
1877
- const { id, status } = req.body;
1878
-
1879
- // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
1880
- // 1. Search for all documents matching the ticket IDs
1881
- const searchBody = {
1882
- query: {
1883
- bool: {
1884
- must: [
1885
- {
1886
- terms: {
1887
- 'id.keyword': id,
1888
- },
1889
- },
1890
- ],
1891
- },
1892
- },
1893
- _source: [ '_id' ], // Only bring _id for efficiency
1894
- };
1895
-
1896
- const searchResp = await getOpenSearchData(
1897
- openSearch.revop,
1898
- searchBody,
1899
- );
1900
- logger.info( { searchResp: searchResp } );
1901
- // Extract bulk IDs to update
1902
- const hits = searchResp?.body?.hits?.hits ?? [];
1903
- logger.info( { hits: hits } );
1904
- if ( !hits.length ) {
1905
- return res.sendError( 'no data', 204 );
1906
- }
1907
-
1908
- // 2. Build bulk update commands
1909
- // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
1910
-
1911
- // 1. Get all IDs from hits
1912
- const docIdToIndex = {};
1913
- hits.forEach( ( doc ) => {
1914
- docIdToIndex[doc._id] = doc._index;
1915
- } );
1916
- const docIds = hits.map( ( doc ) => doc._id );
1917
- logger.info( { docIds } );
1918
- // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
1919
- const getBody = [];
1920
- for ( const doc of hits ) {
1921
- getBody.push( { _index: doc._index, _id: doc._id } );
1922
- }
1923
-
1924
- let mgetResp;
1925
- try {
1926
- mgetResp = await getOpenSearchData(
1927
- openSearch.revop,
1928
- {
1929
- query: {
1930
- ids: {
1931
- values: docIds,
1932
- },
1933
- },
1934
- _source: true,
1935
- },
1936
- );
1937
- } catch ( err ) {
1938
- logger.error( { error: err } );
1939
- mgetResp = undefined;
1940
- }
1941
- logger.info( { mgetResp } );
1942
- // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
1943
- // For fallback, fetch all source fields via another search
1944
- let fullDocs = [];
1945
- if ( mgetResp && mgetResp.body && mgetResp.body.docs && Array.isArray( mgetResp.body.docs ) ) {
1946
- fullDocs = mgetResp.body.docs;
1947
- } else if ( searchResp.body && searchResp.body.hits && searchResp.body.hits.hits ) {
1948
- // fallback: use searchResp docs (request _source above)
1949
- fullDocs = searchResp.body.hits.hits;
1950
- }
1951
-
1952
- // 3. Prepare the new actions array for each doc, and set up bulk update payloads
1953
- const reviewActions = [ 'approved', 'rejected' ];
1954
- const docsToUpdate = [];
1955
- logger.info( { fullDocs: fullDocs } );
1956
- for ( const doc of fullDocs ) {
1957
- const source = doc._source || doc.fields || {}; // support mget and search hits
1958
- let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
1959
- if ( reviewActions.includes( status ) ) {
1960
- // for review: update or push 'review'
1961
- let found = false;
1962
- actions = actions.map( ( item ) => {
1963
- if ( item.actionType === 'review' ) {
1964
- found = true;
1965
- return { ...item, action: status };
1966
- }
1967
- return item;
1968
- } );
1969
- if ( !found ) {
1970
- actions.push( { actionType: 'review', action: status } );
1971
- }
1972
- } else {
1973
- // tagging: update or push 'tagging'
1974
- let found = false;
1975
- actions = actions.map( ( item ) => {
1976
- if ( item.actionType === 'tagging' ) {
1977
- found = true;
1978
- return { ...item, action: 'submitted' };
1979
- }
1980
- return item;
1981
- } );
1982
- if ( !found ) {
1983
- actions.push( { actionType: 'tagging', action: 'submitted' } );
1984
- }
1985
- }
1986
- docsToUpdate.push( {
1987
- _index: doc._index || docIdToIndex[doc._id],
1988
- _id: doc._id,
1989
- actions,
1990
- } );
1991
- }
1992
- const bulkPayload = [];
1993
- // 4. Build bulk update payload
1994
- for ( const doc of docsToUpdate ) {
1995
- bulkPayload.push( {
1996
- update: { _index: doc._index, _id: doc._id },
1997
- } );
1998
- bulkPayload.push( {
1999
- doc: { actions: doc.actions },
2000
- } );
2001
- }
2002
- logger.info( { bulkPayload: bulkPayload } );
2003
-
2004
-
2005
- // 3. Execute bulk update
2006
- const bulkResp = await bulkUpdate( bulkPayload );
2007
-
2008
- // Count successes
2009
- const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2010
-
2011
- logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
2012
-
2013
- return res.sendSuccess( { updated: updatedCount } );
2014
- } catch ( error ) {
2015
- const err = error.message;
2016
- logger.info( { error: err, function: 'updateTempStatus' } );
2017
- return res.sendError( err, 500 );
2018
- }
2019
- }
2020
-
@@ -3,19 +3,79 @@ import dayjs from 'dayjs';
3
3
 
4
4
  export const createTicketSchema = Joi.object().keys( {
5
5
 
6
- dateString: Joi.string().required(),
6
+ dateString: Joi.string().required().custom( ( value, helpers ) => {
7
+ const inputDate = dayjs( value, 'YYYY-MM-DD', true );
8
+ const today = dayjs();
9
+
10
+ if ( !inputDate.isValid() ) {
11
+ return helpers.error( 'any.invalid' );
12
+ }
13
+
14
+ const diff = today.diff( inputDate, 'day' );
15
+
16
+ if ( diff > 3 ) {
17
+ return helpers.message( 'Ticket Creation is not allowed for a period exceeding 3 days' );
18
+ }
19
+
20
+ return value;
21
+ } ),
7
22
  storeId: Joi.string().required(),
8
23
  ticketName: Joi.string().required(),
9
- type: Joi.string()
10
- .required()
11
- .valid( 'create', 'review', 'approve', 'tangRreview' )
12
- .messages( {
13
- 'any.only': 'type must be one of [create, review, approve, tangRreview]',
14
- } ),
24
+ footfallCount: Joi.number().optional(),
25
+ duplicateCount: Joi.number().optional(),
26
+ employeeCount: Joi.number().optional(),
27
+ houseKeepingCount: Joi.number().optional(),
28
+ junkCount: Joi.number().optional(),
29
+ comments: Joi.string().optional().allow( '' ),
30
+ duplicateImages: Joi.array().items(
31
+ Joi.object( {
32
+ tempId: Joi.number().required(),
33
+ filePath: Joi.string().required(),
34
+ entryTime: Joi.string().required(),
35
+ exitTime: Joi.string().required(),
36
+ timeRange: Joi.string().required(),
37
+ isChecked: Joi.boolean().required(),
38
+ data: Joi.array().items(
39
+ Joi.object( {
40
+ tempId: Joi.number().required(),
41
+ filePath: Joi.string().required(),
42
+ entryTime: Joi.string().required(),
43
+ exitTime: Joi.string().required(),
44
+ timeRange: Joi.string().required(),
45
+ isChecked: Joi.boolean().required(),
46
+ } ),
47
+ ).optional(),
48
+ } ) ).optional(),
49
+
50
+ houseKeeping: Joi.array().items( Joi.object( {
51
+ tempId: Joi.number().required(),
52
+ filePath: Joi.string().required(),
53
+ entryTime: Joi.string().required(),
54
+ exitTime: Joi.string().required(),
55
+ timeRange: Joi.string().required(),
56
+ isChecked: Joi.boolean().required(),
57
+ } ) ).optional(),
58
+
59
+ employee: Joi.array().items( Joi.object( {
60
+ tempId: Joi.number().required(),
61
+ filePath: Joi.string().required(),
62
+ entryTime: Joi.string().required(),
63
+ exitTime: Joi.string().required(),
64
+ timeRange: Joi.string().required(),
65
+ isChecked: Joi.boolean().required(),
66
+
67
+ } ) ).optional(),
68
+
69
+ junk: Joi.array().items( Joi.object( {
70
+ tempId: Joi.number().required(),
71
+ filePath: Joi.string().required(),
72
+ entryTime: Joi.string().required(),
73
+ exitTime: Joi.string().required(),
74
+ timeRange: Joi.string().required(),
75
+ isChecked: Joi.boolean().required(),
76
+
77
+ } ) ).optional(),
15
78
 
16
- mode: Joi.string().valid( 'mobile', 'web' ).required().messages( {
17
- 'any.only': 'type must be one of [mobile,web]',
18
- } ),
19
79
 
20
80
  } );
21
81
 
@@ -94,7 +154,6 @@ export const ticketListSchema = Joi.object().keys( {
94
154
  isExport: Joi.boolean().optional(),
95
155
  sortBy: Joi.string().optional().allow( '' ),
96
156
  sortOrder: Joi.number().valid( -1, 1 ).optional(),
97
- tangoType: Joi.string().valid( 'store', 'internal', '' ).optional(),
98
157
  fromDate: Joi.string()
99
158
  .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
100
159
  .required()
@@ -439,52 +498,3 @@ export const downloadTicketsSchema = Joi.object().keys( {
439
498
  export const downloadTicketsValid = {
440
499
  query: downloadTicketsSchema,
441
500
  };
442
-
443
- export const reviewerListSchema = Joi.object().keys( {
444
- clientId: Joi.string().required(),
445
- } );
446
-
447
- export const reviewerListValid = {
448
- query: reviewerListSchema,
449
- };
450
-
451
- export const openTicketListSchema = Joi.object().keys( {
452
- fromDate: Joi.string().required(),
453
- toDate: Joi.string().required(),
454
- clientId: Joi.array().items(
455
- Joi.string().required(),
456
- ).required(),
457
-
458
-
459
- } );
460
-
461
- export const openTicketListValid = {
462
- body: openTicketListSchema,
463
- };
464
-
465
-
466
- export const assignTicketSchema = Joi.object().keys( {
467
- email: Joi.string().required(),
468
- userName: Joi.string().optional(),
469
- role: Joi.string().optional(),
470
- actionType: Joi.string().required(),
471
- ticketId: Joi.string().required(),
472
-
473
-
474
- } );
475
-
476
- export const assignTicketValid = {
477
- body: assignTicketSchema,
478
- };
479
-
480
- export const updateTempStatusSchema = Joi.object().keys( {
481
- id: Joi.array().items( Joi.string().required() ).required(),
482
- status: Joi.string().required(),
483
- type: Joi.string().required(),
484
-
485
-
486
- } );
487
-
488
- export const updateTempStatusValid = {
489
- body: updateTempStatusSchema,
490
- };
@@ -1,23 +1,17 @@
1
1
  import express from 'express';
2
- import { getClusters, getConfig, isGrantedUsers, isTicketExists, ticketCreation } from '../validations/footfallDirectory.validation.js';
3
- import { assignTicket, createTicket, downloadTickets, getTaggedStores, getTickets, openTicketList, reviewerList, ticketList, ticketSummary, updateStatus, updateTempStatus } from '../controllers/footfallDirectory.controllers.js';
4
- import { createTicketValid, downloadTicketsValid, getTaggedStoresValid, getTicketsValid, openTicketListValid, reviewerListValid, ticketListValid, ticketSummaryValid, updateStatusValid, assignTicketValid, updateTempStatusValid } from '../dtos/footfallDirectory.dtos.js';
2
+ import { getClusters, isExist, isTicketExists } from '../validations/footfallDirectory.validation.js';
3
+ import { createTicket, downloadTickets, getTaggedStores, getTickets, ticketList, ticketSummary, updateStatus } from '../controllers/footfallDirectory.controllers.js';
4
+ import { createTicketValid, downloadTicketsValid, getTaggedStoresValid, getTicketsValid, ticketListValid, ticketSummaryValid, updateStatusValid } from '../dtos/footfallDirectory.dtos.js';
5
5
  import { bulkValidate, getAssinedStore, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
6
6
 
7
7
  export const footfallDirectoryRouter = express.Router();
8
8
 
9
- footfallDirectoryRouter.post( '/create-ticket', isAllowedSessionHandler, validate( createTicketValid ), isGrantedUsers, getConfig, ticketCreation, createTicket );
9
+ footfallDirectoryRouter.post( '/create-ticket', isAllowedSessionHandler, validate( createTicketValid ), isExist, createTicket );
10
10
  footfallDirectoryRouter.get( '/ticket-summary', isAllowedSessionHandler, bulkValidate( ticketSummaryValid ), ticketSummary );
11
11
 
12
12
  footfallDirectoryRouter.get( '/ticket-list', isAllowedSessionHandler, bulkValidate( ticketListValid ), ticketList );
13
- // footfallDirectoryRouter.get( '/ticket-list', isAllowedSessionHandler, bulkValidate( ticketListValid ), ticketList );
14
13
  footfallDirectoryRouter.get( '/get-tickets', isAllowedSessionHandler, bulkValidate( getTicketsValid ), getTickets );
15
14
  footfallDirectoryRouter.get( '/get-tagged-stores', isAllowedSessionHandler, bulkValidate( getTaggedStoresValid ), getAssinedStore, getClusters, getTaggedStores );
16
15
  footfallDirectoryRouter.put( '/update-status', isAllowedSessionHandler, bulkValidate( updateStatusValid ), updateStatus );
17
16
  footfallDirectoryRouter.get( '/download-tickets', isAllowedSessionHandler, bulkValidate( downloadTicketsValid ), isTicketExists, downloadTickets );
18
- footfallDirectoryRouter.get( '/reviewer-list', isAllowedSessionHandler, bulkValidate( reviewerListValid ), reviewerList );
19
- footfallDirectoryRouter.post( '/open-ticket-list', isAllowedSessionHandler, bulkValidate( openTicketListValid ), openTicketList );
20
- footfallDirectoryRouter.post( '/assign-ticket', isAllowedSessionHandler, bulkValidate( assignTicketValid ), assignTicket );
21
- footfallDirectoryRouter.post( '/update-temp-status', isAllowedSessionHandler, bulkValidate( updateTempStatusValid ), updateTempStatus );
22
-
23
17
 
@@ -1,55 +1,6 @@
1
- import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger } from 'tango-app-api-middleware';
1
+ import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
2
2
  import { aggregateCluster } from '../services/cluster.service.js';
3
3
  import { findOneRevopDownload } from '../services/revopDownload.service.js';
4
- import { findOneStore } from '../services/store.service.js';
5
- import { findOneClient } from '../services/client.service.js';
6
- import { updateOneUpsertVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
7
-
8
- function formatRevopTaggingHits( hits = [] ) {
9
- return hits
10
- .map( ( hit ) => {
11
- const source = hit?._source || {};
12
- if ( Object.keys( source ).length === 0 ) {
13
- return null;
14
- }
15
-
16
- const duplicateImages = Array.isArray( source.duplicateImage ) ?
17
- source.duplicateImage.map( ( item ) => ( {
18
- tempId: item?.tempId,
19
- timeRange: item?.timeRange,
20
- entryTime: item?.entryTime,
21
- exitTime: item?.exitTime,
22
- filePath: item?.filePath,
23
- status: item?.status,
24
- isChecked: Boolean( item?.isChecked ),
25
- } ) ) :
26
- [];
27
-
28
- return {
29
- id: hit?._id,
30
- clientId: source?.clientId,
31
- storeId: source?.storeId,
32
- tempId: source?.tempId,
33
- dateString: source?.dateString,
34
- timeRange: source?.timeRange,
35
- processType: source?.processType,
36
- revopsType: source?.revopsType,
37
- entryTime: source?.entryTime,
38
- exitTime: source?.exitTime,
39
- filePath: source?.filePath,
40
- status: source?.status,
41
- description: source?.description || '',
42
- isChecked: Boolean( source?.isChecked ),
43
- type: source?.type,
44
- parent: source?.parent ?? null,
45
- isParent: duplicateImages.length > 0 && !source?.parent,
46
- createdAt: source?.createdAt,
47
- updatedAt: source?.updatedAt,
48
- data: duplicateImages,
49
- };
50
- } )
51
- .filter( Boolean );
52
- }
53
4
 
54
5
  export async function isExist( req, res, next ) {
55
6
  try {
@@ -178,507 +129,3 @@ export async function isTicketExists( req, res, next ) {
178
129
  }
179
130
  }
180
131
 
181
- export async function isGrantedUsers( req, res, next ) {
182
- try {
183
- const userInfo = req?.user;
184
- switch ( userInfo.userType ) {
185
- case 'client':
186
- const ticketsFeature = userInfo?.rolespermission?.find( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Reviewer' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Approver' && m.isAdd==true ) ) );
187
- logger.info( { ticketsFeature } );
188
- if ( ticketsFeature ) {
189
- return next();
190
- } else {
191
- return res.sendError( 'Forbidden to this action', 403 );
192
- }
193
-
194
- case 'tango':
195
- return next();
196
- default:
197
- return res.sendError( 'userType doesnot match', 400 );
198
- }
199
- } catch ( error ) {
200
- const err = error.message || 'Internal Server Error';
201
- return res.sendError( err, 500 );
202
- }
203
- }
204
-
205
- export async function getConfig( req, res, next ) {
206
- try {
207
- const inputData = req.body;
208
- const storeKey = inputData.storeId.split( '-' )[0];
209
-
210
- const config = await findOneClient( { clientId: storeKey }, { footfallDirectoryConfigs: 1 } );
211
- logger.info( { config, storeKey } );
212
- const accuracyBreach = config?.footfallDirectoryConfigs?.accuracyBreach;
213
- req.accuracyBreach = accuracyBreach || '';
214
- return next();
215
- } catch ( error ) {
216
- const err = error.message || 'Internal Server Error';
217
- return res.sendError( err, 500 );
218
- }
219
- }
220
-
221
-
222
- export async function ticketCreation( req, res, next ) {
223
- try {
224
- const inputData = req.body;
225
- if ( inputData?.type !== 'create' ) {
226
- return next();
227
- }
228
- // check the createtion permission from the user permission
229
- const userInfo = req?.user;
230
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && ( m.isAdd==true || m.isEdit==true ) ) ) );
231
- if ( !ticketsFeature ) {
232
- return res.sendError( 'Forbidden to Create Ticket', 403 );
233
- }
234
-
235
- // get store info by the storeId into mongo db
236
- const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
237
-
238
- if ( !getstoreName || getstoreName == null ) {
239
- return res.sendError( 'The store ID is either inActive or not found', 400 );
240
- }
241
-
242
- // get the footfall count from opensearch
243
- const openSearch = JSON.parse( process.env.OPENSEARCH );
244
- const dateString = `${inputData.storeId}_${inputData.dateString}`;
245
- const getQuery = {
246
- query: {
247
- terms: {
248
- _id: [ dateString ],
249
- },
250
- },
251
- _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
252
- sort: [
253
- {
254
- date_iso: {
255
- order: 'desc',
256
- },
257
- },
258
- ],
259
- };
260
-
261
- const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
262
- const hits = getFootfallCount?.body?.hits?.hits || [];
263
- if ( hits?.[0]?._source?.footfall_count <= 0 ) {
264
- return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
265
- }
266
-
267
- // get category details from the client level configuration
268
- const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
269
- if ( !getConfig || getConfig == null ) {
270
- return res.sendError( 'The Client ID is either not configured or not found', 400 );
271
- }
272
-
273
- // Get taggingLimitation from config (check both possible paths)
274
- const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
275
- // Initialize count object from taggingLimitation
276
- const tempAcc = [];
277
- const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
278
- if ( item?.type ) {
279
- // Convert type to camelCase with "Count" suffix
280
- // e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
281
- const typeLower = item.type.toLowerCase();
282
- let key;
283
- if ( typeLower === 'housekeeping' ) {
284
- key = 'houseKeepingCount';
285
- } else {
286
- // Convert first letter to lowercase and append "Count"
287
- key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
288
- }
289
-
290
-
291
- // To change from an object to the desired array structure, assemble an array of objects:
292
- tempAcc.push( {
293
- name: item.name,
294
- value: 0,
295
- key: key,
296
- type: item.type,
297
- } );
298
-
299
-
300
- return acc;
301
- }
302
- }, {} ) || {};
303
-
304
- // Query OpenSearch revop index to get actual counts for each type
305
- if ( taggingLimitation && taggingLimitation.length > 0 ) {
306
- const revopQuery = {
307
- size: 0,
308
- query: {
309
- bool: {
310
- must: [
311
- {
312
- term: {
313
- 'storeId.keyword': inputData.storeId,
314
- },
315
- },
316
- {
317
- term: {
318
- 'dateString': inputData.dateString,
319
- },
320
- },
321
- ],
322
- },
323
- },
324
- aggs: {
325
- type_counts: {
326
- terms: {
327
- field: 'revopsType.keyword',
328
- size: 100,
329
- },
330
- },
331
- },
332
- };
333
-
334
-
335
- const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
336
- const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
337
-
338
- // Map OpenSearch revopsType values to count object keys
339
- buckets.forEach( ( bucket ) => {
340
- const revopsType = bucket.key;
341
- const count = bucket.doc_count || 0;
342
-
343
-
344
- if ( Array.isArray( tempAcc ) ) {
345
- // Find the tempAcc entry whose type (case-insensitive) matches revopsType
346
- const accMatch = tempAcc.find(
347
- ( acc ) =>
348
- acc.type &&
349
- acc.type === revopsType,
350
- );
351
-
352
- if ( accMatch && accMatch.key ) {
353
- tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
354
- }
355
- }
356
- } );
357
- }
358
-
359
-
360
- // Calculate revisedFootfall: footfallCount - (sum of all counts)
361
-
362
- const totalCount = Array.isArray( tempAcc ) ?
363
- tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
364
- 0;
365
- const footfallCount = hits?.[0]?._source?.footfall_count || 0;
366
- const revisedFootfall = Math.max( 0, footfallCount - totalCount );
367
- if ( footfallCount - revisedFootfall == 0 ) {
368
- return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
369
- }
370
-
371
- const taggingData = {
372
- size: 10000,
373
- query: {
374
- bool: {
375
- must: [
376
- {
377
- term: {
378
- 'storeId.keyword': inputData.storeId,
379
- },
380
- },
381
- {
382
- term: {
383
- 'dateString': inputData.dateString,
384
- },
385
- },
386
- ],
387
- },
388
- },
389
- };
390
-
391
- const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
392
- const taggingImages = revopTaggingData?.body?.hits?.hits;
393
- if ( !taggingImages || taggingImages?.length == 0 ) {
394
- return res.sendError( 'You don’t have any tagged images right now', 400 );
395
- }
396
- const formattedTaggingData = formatRevopTaggingHits( taggingImages );
397
-
398
- const record = {
399
- storeId: inputData.storeId,
400
- type: 'store',
401
- dateString: inputData.dateString,
402
- storeName: getstoreName?.storeName,
403
- ticketName: inputData.ticketName|| 'footfall-directory',
404
- footfallCount: footfallCount,
405
- clientId: getstoreName?.clientId,
406
- ticketId: 'TE_FDT_' + new Date().valueOf(),
407
- createdAt: new Date(),
408
- updatedAt: new Date(),
409
- status: 'raised',
410
- revicedFootfall: revisedFootfall,
411
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
412
- mappingInfo: [
413
- {
414
- type: 'tagging',
415
- mode: inputData.mode,
416
- revicedFootfall: revisedFootfall,
417
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
418
- count: tempAcc,
419
- revisedDetail: formattedTaggingData,
420
- status: 'raised',
421
- createdByEmail: req?.user?.email,
422
- createdByUserName: req?.user?.userName,
423
- createdByRole: req?.user?.role,
424
- createdAt: new Date(),
425
- },
426
- ],
427
- };
428
-
429
-
430
- // Retrieve client footfallDirectoryConfigs revision
431
- let isAutoCloseEnable = false;
432
- let autoCloseAccuracy = '95%';
433
- try {
434
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
435
- if ( clientData?.footfallDirectoryConfigs ) {
436
- isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
437
- autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
438
- }
439
- } catch ( e ) {
440
- isAutoCloseEnable = false;
441
- autoCloseAccuracy = '95%';
442
- }
443
-
444
- let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || '95' ).replace( '%', '' ) );
445
- let revisedPercentage = 0;
446
- if ( typeof getCategory === 'number' && getCategory > 0 ) {
447
- revisedPercentage = ( revisedFootfall / getCategory ) * 100;
448
- }
449
-
450
- // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
451
- if (
452
- isAutoCloseEnable === true &&
453
- revisedPercentage >= autoCloseAccuracyValue
454
- ) {
455
- record.status = 'closed';
456
- record.mappingInfo = [
457
- {
458
- type: 'tagging',
459
- mode: inputData.mode,
460
- revicedFootfall: revisedFootfall,
461
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
462
- count: getCategory,
463
- revisedDetail: formattedTaggingData,
464
- status: 'closed',
465
- createdByEmail: req?.user?.email,
466
- createdByUserName: req?.user?.userName,
467
- createdByRole: req?.user?.role,
468
- },
469
- ];
470
- } else {
471
- // If ticket is closed, do not proceed with revision mapping
472
- let revisionArray = [];
473
- try {
474
- const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
475
- revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
476
- } catch ( e ) {
477
- revisionArray = [];
478
- }
479
-
480
- // Default fallbacks
481
- let revisionMapping = null;
482
- let approverMapping = null;
483
- let tangoReviewMapping = null;
484
-
485
- // Find out which roles have isChecked true
486
- if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
487
- for ( const r of revisionArray ) {
488
- if ( r.actionType === 'reviewer' && r.isChecked === true ) {
489
- revisionMapping = {
490
- type: 'review',
491
- revicedFootfall: revisedFootfall,
492
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
493
- count: getCategory,
494
- revisedDetail: formattedTaggingData,
495
- status: 'open',
496
- };
497
- } else if ( r.actionType === 'approver' && r.isChecked === true ) {
498
- approverMapping = {
499
- type: 'approver',
500
- revicedFootfall: revisedFootfall,
501
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
502
- count: getCategory,
503
- revisedDetail: formattedTaggingData,
504
- status: 'open',
505
- };
506
- } else if ( r.actionType === 'tango' && r.isChecked === true ) {
507
- tangoReviewMapping = {
508
- type: 'tango-review',
509
- revicedFootfall: revisedFootfall,
510
- revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
511
- count: getCategory,
512
- revisedDetail: formattedTaggingData,
513
- status: 'open',
514
- };
515
- }
516
- }
517
- }
518
-
519
- // Insert appropriate mappingInfo blocks
520
- if ( revisionMapping ) {
521
- // If reviewer and checked
522
- record.mappingInfo.push( revisionMapping );
523
- } else if ( approverMapping ) {
524
- // If approver and checked
525
- record.mappingInfo.push( approverMapping );
526
- } else if ( tangoReviewMapping ) {
527
- // If none above, then tangoReview
528
- record.mappingInfo.push( tangoReviewMapping );
529
- }
530
- }
531
-
532
-
533
- const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
534
- const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
535
- if ( insertResult && insertResult.statusCode === 201 ) {
536
- // After successful ticket creation, update status to "submitted" in revop index for the relevant records
537
- try {
538
- const bulkUpdateBody = taggingImages.map( ( img ) => [
539
- { update: { _index: openSearch.revop, _id: img._id } },
540
- { doc: { status: 'submitted' } },
541
- ] ).flat();
542
-
543
- if ( bulkUpdateBody.length > 0 ) {
544
- await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
545
- }
546
- } catch ( updateErr ) {
547
- logger.error( { error: updateErr, message: 'Failed to update status to submitted in revop index' } );
548
- // Do not block the success response for this failure
549
- }
550
-
551
- // Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
552
-
553
- if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
554
- const breachDays = Number( req.accuracyBreach.days );
555
- const breachCount = Number( req.accuracyBreach.ticketCount );
556
-
557
- // req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
558
- const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
559
- const storeId = inputData.storeId;
560
- const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
561
-
562
-
563
- const formatDate = ( d ) =>
564
- `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
565
-
566
- // Compute current date object
567
-
568
-
569
- // Calculate start date based on config days
570
- // If 30 days: start from first day of current month
571
- // If 60 days: start from first day of last month
572
- const currentDateObj = new Date(); // Declare currentDateObj as today's date
573
- const startDateObj = new Date( currentDateObj );
574
-
575
- if ( breachDays === 30 ) {
576
- // Consider within this month
577
- startDateObj.setDate( 1 ); // First day of current month
578
- } else if ( breachDays === 60 ) {
579
- // Consider this month and last month
580
- startDateObj.setMonth( startDateObj.getMonth() - 1 );
581
- startDateObj.setDate( 1 ); // First day of last month
582
- } else {
583
- // For other values, calculate months from days
584
- const breachMonths = Math.ceil( breachDays / 30 );
585
- startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
586
- startDateObj.setDate( 1 );
587
- }
588
-
589
-
590
- const startDate = startDateObj;
591
-
592
-
593
- const endDate = new Date;
594
- // Query for tickets within this months window for this store and ticket name
595
- const query = {
596
- query: {
597
- bool: {
598
- must: [
599
- { term: { 'storeId.keyword': storeId } },
600
- { term: { 'ticketName.keyword': ticketName } },
601
- { range: { createdAt: { gte: startDate, lte: endDate } } },
602
- ],
603
- },
604
- },
605
- };
606
-
607
- // Search in OpenSearch for recent similar tickets
608
- const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
609
- const tickets = breachTicketsResult?.body?.hits?.hits || [];
610
-
611
- // Filter tickets where revised footfall percentage > config accuracy
612
- let breachTicketsCount = 0;
613
- for ( const ticket of tickets ) {
614
- const source = ticket._source || {};
615
- const footfallCount = source.footfallCount || 0;
616
- const revicedFootfall = source.mappingInfo?.[0]?.revicedFootfall || 0;
617
-
618
- // Calculate revised footfall percentage
619
- let revisedPercentage = 0;
620
- if ( footfallCount > 0 ) {
621
- revisedPercentage = ( revicedFootfall / footfallCount ) * 100;
622
- }
623
-
624
- // Check if revised footfall percentage > config accuracy
625
- if ( revisedPercentage > breachAccuracy ) {
626
- breachTicketsCount++;
627
- }
628
- }
629
-
630
- if ( breachTicketsCount >= breachCount ) {
631
- // Calculate remaining future days in the config period
632
- const futureDates = [];
633
-
634
- // Calculate end date of config period
635
- const configEndDateObj = new Date( currentDateObj );
636
- if ( breachDays === 30 ) {
637
- // End of current month
638
- configEndDateObj.setMonth( configEndDateObj.getMonth() + 1 );
639
- configEndDateObj.setDate( 0 ); // Last day of current month
640
- } else if ( breachDays === 60 ) {
641
- // End of next month
642
- configEndDateObj.setMonth( configEndDateObj.getMonth() + 2 );
643
- configEndDateObj.setDate( 0 ); // Last day of next month
644
- } else {
645
- // For other values, add the remaining days
646
- const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
647
- configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
648
- }
649
-
650
- // Generate all dates from tomorrow until end of config period
651
- const tomorrow = new Date( currentDateObj );
652
- tomorrow.setDate( tomorrow.getDate() + 1 );
653
-
654
- const dateIterator = new Date( tomorrow );
655
- while ( dateIterator <= configEndDateObj ) {
656
- futureDates.push( formatDate( dateIterator ) );
657
- dateIterator.setDate( dateIterator.getDate() + 1 );
658
- }
659
-
660
- // Insert a record for each future date
661
- for ( const futureDateString of futureDates ) {
662
- const record = {
663
- clientId: getstoreName?.clientId,
664
- storeId: inputData?.storeId,
665
- storeName: getstoreName?.storeName,
666
- dateString: futureDateString,
667
- status: 'block',
668
- };
669
-
670
- await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
671
- }
672
- }
673
- }
674
-
675
- return res.sendSuccess( 'Ticket raised successfully' );
676
- }
677
- } catch ( error ) {
678
- const err = error.message || 'Internal Server Error';
679
- logger.error( { error: err, funtion: 'ticketCreation' } );
680
- return res.sendError( err, 500 );
681
- }
682
- }
683
-
684
-
@@ -1,4 +0,0 @@
1
- import vmsStoreRequestModel from 'tango-api-schema/schema/vmsStoreRequest.model.js';
2
- export async function updateOneUpsertVmsStoreRequest( query, record ) {
3
- return await vmsStoreRequestModel.updateOne( query, { $set: record }, { upsert: true } );
4
- };