tango-app-api-infra 3.9.5-vms.6 → 3.9.5-vms.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-infra",
3
- "version": "3.9.5-vms.6",
3
+ "version": "3.9.5-vms.8",
4
4
  "description": "infra",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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.20",
30
+ "tango-api-schema": "^2.4.28",
31
31
  "tango-app-api-middleware": "^3.1.93",
32
32
  "winston": "^3.12.0",
33
33
  "winston-daily-rotate-file": "^5.0.0"
@@ -497,7 +497,7 @@ export async function ticketList( req, res ) {
497
497
  const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
498
498
 
499
499
  if ( req.user.userType =='tango' ) {
500
- result =inputData.tangotyep == 'store'?
500
+ result =inputData.tangotype == 'store'?
501
501
 
502
502
 
503
503
  [
@@ -716,18 +716,18 @@ export async function ticketList( req, res ) {
716
716
  [
717
717
 
718
718
  {
719
- ticketId: 'TE_FDT_1763539990306',
719
+ ticketId: 'TE_FDT_1763860421803',
720
720
  storeId: '11-1716',
721
721
  storeName: 'LKST1916',
722
- ticketRaised: '2025-11-16',
723
- issueDate: '2025-11-16',
724
- footfall: 200,
725
- type: 'internal',
726
- storeRevisedAccuracy: '98%',
727
- reviewerRevisedAccuracy: '97%',
728
- approverRevisedAccuracy: '98%',
729
- tangoRevisedAccuracy: '98%',
730
- status: 'Closed',
722
+ ticketRaised: '2025-11-21',
723
+ issueDate: '2025-11-20',
724
+ footfall: 94,
725
+ type: 'store',
726
+ storeRevisedAccuracy: '95%',
727
+ reviewerRevisedAccuracy: '--',
728
+ approverRevisedAccuracy: '--',
729
+ tangoRevisedAccuracy: '--',
730
+ status: 'Open',
731
731
  tangoStatus: 'Open',
732
732
  },
733
733
  {
@@ -1023,13 +1023,13 @@ req.user.role === 'user'? 'NA':
1023
1023
  ticketsFeature?
1024
1024
  [
1025
1025
  {
1026
- ticketId: 'TE_FDT_17635399903490',
1026
+ ticketId: 'TE_FDT_1763860421803',
1027
1027
  storeId: '11-1716',
1028
1028
  storeName: 'LKST1916',
1029
- ticketRaised: '2025-11-16',
1030
- issueDate: '2025-11-15',
1029
+ ticketRaised: '2025-11-21',
1030
+ issueDate: '2025-11-20',
1031
1031
  dueDate: 'Due Today',
1032
- footfall: 213,
1032
+ footfall: 90,
1033
1033
  storeRevisedAccuracy: '90%',
1034
1034
  reviewerRevisedAccuracy: '0%',
1035
1035
  status: 'Open',
@@ -1039,13 +1039,13 @@ ticketsFeature?
1039
1039
  ticketId: 'TE_FDT_1763539990346',
1040
1040
  storeId: '11-2000',
1041
1041
  storeName: 'LKST2368',
1042
- ticketRaised: '2025-11-16',
1043
- issueDate: '2025-11-14',
1044
- dueDate: '2025-11-18',
1042
+ ticketRaised: '2025-11-21',
1043
+ issueDate: '2025-11-20',
1044
+ dueDate: '2025-11-26',
1045
1045
  footfall: 90,
1046
1046
  storeRevisedAccuracy: '90%',
1047
1047
  reviewerRevisedAccuracy: '--',
1048
- status: 'Expired',
1048
+ status: 'In-Progress',
1049
1049
  ReviewedBy: 'mu_mu@yopmail.com',
1050
1050
  },
1051
1051
  {
@@ -2310,7 +2310,7 @@ export async function openTicketList( req, res ) {
2310
2310
  }
2311
2311
  }
2312
2312
 
2313
- export async function updateiTcket( req, res ) {
2313
+ export async function assignTicket( req, res ) {
2314
2314
  try {
2315
2315
  const inputData = req.body;
2316
2316
  const openSearch = JSON.parse( process.env.OPENSEARCH );
@@ -2384,7 +2384,154 @@ export async function updateiTcket( req, res ) {
2384
2384
  return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
2385
2385
  } catch ( error ) {
2386
2386
  const err = error.message || 'Internal Server Error';
2387
- logger.error( { error: error, function: 'updateticket' } );
2387
+ logger.error( { error: error, function: 'assignTicket' } );
2388
+ return res.sendError( err, 500 );
2389
+ }
2390
+ }
2391
+
2392
+ export async function updateTempStatus( req, res ) {
2393
+ try {
2394
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
2395
+ const { id, status } = req.body;
2396
+
2397
+ // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
2398
+ // 1. Search for all documents matching the ticket IDs
2399
+ const searchBody = {
2400
+ query: {
2401
+ bool: {
2402
+ must: [
2403
+ {
2404
+ terms: {
2405
+ 'id.keyword': id,
2406
+ },
2407
+ },
2408
+ ],
2409
+ },
2410
+ },
2411
+ _source: [ '_id' ], // Only bring _id for efficiency
2412
+ };
2413
+
2414
+ const searchResp = await getOpenSearchData(
2415
+ openSearch.revop,
2416
+ searchBody,
2417
+ );
2418
+ logger.info( { searchResp: searchResp } );
2419
+ // Extract bulk IDs to update
2420
+ const hits = searchResp?.body?.hits?.hits ?? [];
2421
+ logger.info( { hits: hits } );
2422
+ if ( !hits.length ) {
2423
+ return res.sendError( 'no data', 204 );
2424
+ }
2425
+
2426
+ // 2. Build bulk update commands
2427
+ // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
2428
+
2429
+ // 1. Get all IDs from hits
2430
+ const docIdToIndex = {};
2431
+ hits.forEach( ( doc ) => {
2432
+ docIdToIndex[doc._id] = doc._index;
2433
+ } );
2434
+ const docIds = hits.map( ( doc ) => doc._id );
2435
+ logger.info( { docIds } );
2436
+ // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
2437
+ const getBody = [];
2438
+ for ( const doc of hits ) {
2439
+ getBody.push( { _index: doc._index, _id: doc._id } );
2440
+ }
2441
+
2442
+ let mgetResp;
2443
+ try {
2444
+ mgetResp = await getOpenSearchData(
2445
+ openSearch.revop,
2446
+ {
2447
+ query: {
2448
+ ids: {
2449
+ values: docIds,
2450
+ },
2451
+ },
2452
+ _source: true,
2453
+ },
2454
+ );
2455
+ } catch ( err ) {
2456
+ logger.error( { error: err } );
2457
+ mgetResp = undefined;
2458
+ }
2459
+ logger.info( { mgetResp } );
2460
+ // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
2461
+ // For fallback, fetch all source fields via another search
2462
+ let fullDocs = [];
2463
+ if ( mgetResp && mgetResp.body && mgetResp.body.docs && Array.isArray( mgetResp.body.docs ) ) {
2464
+ fullDocs = mgetResp.body.docs;
2465
+ } else if ( searchResp.body && searchResp.body.hits && searchResp.body.hits.hits ) {
2466
+ // fallback: use searchResp docs (request _source above)
2467
+ fullDocs = searchResp.body.hits.hits;
2468
+ }
2469
+
2470
+ // 3. Prepare the new actions array for each doc, and set up bulk update payloads
2471
+ const reviewActions = [ 'approved', 'rejected' ];
2472
+ const docsToUpdate = [];
2473
+ logger.info( { fullDocs: fullDocs } );
2474
+ for ( const doc of fullDocs ) {
2475
+ const source = doc._source || doc.fields || {}; // support mget and search hits
2476
+ let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
2477
+ if ( reviewActions.includes( status ) ) {
2478
+ // for review: update or push 'review'
2479
+ let found = false;
2480
+ actions = actions.map( ( item ) => {
2481
+ if ( item.actionType === 'review' ) {
2482
+ found = true;
2483
+ return { ...item, action: status };
2484
+ }
2485
+ return item;
2486
+ } );
2487
+ if ( !found ) {
2488
+ actions.push( { actionType: 'review', action: status } );
2489
+ }
2490
+ } else {
2491
+ // tagging: update or push 'tagging'
2492
+ let found = false;
2493
+ actions = actions.map( ( item ) => {
2494
+ if ( item.actionType === 'tagging' ) {
2495
+ found = true;
2496
+ return { ...item, action: 'submitted' };
2497
+ }
2498
+ return item;
2499
+ } );
2500
+ if ( !found ) {
2501
+ actions.push( { actionType: 'tagging', action: 'submitted' } );
2502
+ }
2503
+ }
2504
+ docsToUpdate.push( {
2505
+ _index: doc._index || docIdToIndex[doc._id],
2506
+ _id: doc._id,
2507
+ actions,
2508
+ } );
2509
+ }
2510
+ const bulkPayload = [];
2511
+ // 4. Build bulk update payload
2512
+ for ( const doc of docsToUpdate ) {
2513
+ bulkPayload.push( {
2514
+ update: { _index: doc._index, _id: doc._id },
2515
+ } );
2516
+ bulkPayload.push( {
2517
+ doc: { actions: doc.actions },
2518
+ } );
2519
+ }
2520
+ logger.info( { bulkPayload: bulkPayload } );
2521
+
2522
+
2523
+ // 3. Execute bulk update
2524
+ const bulkResp = await bulkUpdate( bulkPayload );
2525
+
2526
+ // Count successes
2527
+ const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2528
+
2529
+ logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
2530
+
2531
+ return res.sendSuccess( { updated: updatedCount } );
2532
+ } catch ( error ) {
2533
+ const err = error.message;
2534
+ logger.info( { error: err, function: 'updateTempStatus' } );
2388
2535
  return res.sendError( err, 500 );
2389
2536
  }
2390
2537
  }
@@ -463,7 +463,7 @@ export const openTicketListValid = {
463
463
  };
464
464
 
465
465
 
466
- export const updateTicketListSchema = Joi.object().keys( {
466
+ export const assignTicketSchema = Joi.object().keys( {
467
467
  email: Joi.string().required(),
468
468
  userName: Joi.string().optional(),
469
469
  role: Joi.string().optional(),
@@ -473,6 +473,18 @@ export const updateTicketListSchema = Joi.object().keys( {
473
473
 
474
474
  } );
475
475
 
476
- export const updateTicketListValid = {
477
- body: updateTicketListSchema,
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,
478
490
  };
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import { getClusters, getConfig, isGrantedUsers, isTicketExists, ticketCreation } from '../validations/footfallDirectory.validation.js';
3
- import { createTicket, downloadTickets, getTaggedStores, getTickets, openTicketList, reviewerList, ticketList, ticketSummary, updateiTcket, updateStatus } from '../controllers/footfallDirectory.controllers.js';
4
- import { createTicketValid, downloadTicketsValid, getTaggedStoresValid, getTicketsValid, openTicketListValid, reviewerListValid, ticketListValid, ticketSummaryValid, updateStatusValid, updateTicketListValid } from '../dtos/footfallDirectory.dtos.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';
5
5
  import { bulkValidate, getAssinedStore, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
6
6
 
7
7
  export const footfallDirectoryRouter = express.Router();
@@ -17,6 +17,7 @@ footfallDirectoryRouter.put( '/update-status', isAllowedSessionHandler, bulkVali
17
17
  footfallDirectoryRouter.get( '/download-tickets', isAllowedSessionHandler, bulkValidate( downloadTicketsValid ), isTicketExists, downloadTickets );
18
18
  footfallDirectoryRouter.get( '/reviewer-list', isAllowedSessionHandler, bulkValidate( reviewerListValid ), reviewerList );
19
19
  footfallDirectoryRouter.post( '/open-ticket-list', isAllowedSessionHandler, bulkValidate( openTicketListValid ), openTicketList );
20
- footfallDirectoryRouter.post( '/update-ticket-user', isAllowedSessionHandler, bulkValidate( updateTicketListValid ), updateiTcket );
20
+ footfallDirectoryRouter.post( '/assign-ticket', isAllowedSessionHandler, bulkValidate( assignTicketValid ), assignTicket );
21
+ footfallDirectoryRouter.post( '/update-temp-status', isAllowedSessionHandler, bulkValidate( updateTempStatusValid ), updateTempStatus );
21
22
 
22
23
 
@@ -228,14 +228,13 @@ export async function ticketCreation( req, res, next ) {
228
228
  // check the createtion permission from the user permission
229
229
  const userInfo = req?.user;
230
230
  const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && ( m.isAdd==true || m.isEdit==true ) ) ) );
231
- logger.info( { ticketsFeature } );
232
231
  if ( !ticketsFeature ) {
233
232
  return res.sendError( 'Forbidden to Create Ticket', 403 );
234
233
  }
235
234
 
236
235
  // get store info by the storeId into mongo db
237
236
  const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
238
- logger.info( { getstoreName } );
237
+
239
238
  if ( !getstoreName || getstoreName == null ) {
240
239
  return res.sendError( 'The store ID is either inActive or not found', 400 );
241
240
  }
@@ -261,22 +260,20 @@ export async function ticketCreation( req, res, next ) {
261
260
 
262
261
  const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
263
262
  const hits = getFootfallCount?.body?.hits?.hits || [];
264
- logger.info( { hits } );
265
263
  if ( hits?.[0]?._source?.footfall_count <= 0 ) {
266
264
  return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
267
265
  }
268
266
 
269
267
  // get category details from the client level configuration
270
268
  const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
271
- logger.info( { getConfig, ta123: getConfig?.footfallDirectoryConfigs } );
272
269
  if ( !getConfig || getConfig == null ) {
273
270
  return res.sendError( 'The Client ID is either not configured or not found', 400 );
274
271
  }
275
272
 
276
273
  // Get taggingLimitation from config (check both possible paths)
277
274
  const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
278
- logger.info( { taggingLimitation, tagginngs: getConfig?.footfallDirectoryConfigs } );
279
275
  // Initialize count object from taggingLimitation
276
+ const tempAcc = [];
280
277
  const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
281
278
  if ( item?.type ) {
282
279
  // Convert type to camelCase with "Count" suffix
@@ -289,11 +286,21 @@ export async function ticketCreation( req, res, next ) {
289
286
  // Convert first letter to lowercase and append "Count"
290
287
  key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
291
288
  }
292
- acc[key] = 0;
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;
293
301
  }
294
- return acc;
295
302
  }, {} ) || {};
296
- logger.info( { getCategory } );
303
+
297
304
  // Query OpenSearch revop index to get actual counts for each type
298
305
  if ( taggingLimitation && taggingLimitation.length > 0 ) {
299
306
  const revopQuery = {
@@ -324,51 +331,38 @@ export async function ticketCreation( req, res, next ) {
324
331
  },
325
332
  };
326
333
 
327
- try {
328
- const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
329
- logger.info( { revopData: revopData } );
330
- const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
331
-
332
- // Map OpenSearch revopsType values to count object keys
333
- buckets.forEach( ( bucket ) => {
334
- const revopsType = bucket.key?.toLowerCase();
335
- const count = bucket.doc_count || 0;
336
-
337
- // Map the revopsType to the correct count key
338
- // Handle variations: duplicate, duplicateImages, duplicateimages
339
- if ( revopsType === 'duplicate' ) {
340
- if ( getCategory.hasOwnProperty( 'duplicateCount' ) ) {
341
- getCategory.duplicateCount = count;
342
- }
343
- } else if ( revopsType === 'employee' ) {
344
- if ( getCategory.hasOwnProperty( 'employeeCount' ) ) {
345
- getCategory.employeeCount = count;
346
- }
347
- } else if ( revopsType === 'housekeeping' ) {
348
- if ( getCategory.hasOwnProperty( 'houseKeepingCount' ) ) {
349
- getCategory.houseKeepingCount = count;
350
- }
351
- } else if ( revopsType === 'junk' ) {
352
- if ( getCategory.hasOwnProperty( 'junkCount' ) ) {
353
- getCategory.junkCount = count;
354
- }
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;
355
354
  }
356
- } );
357
- } catch ( error ) {
358
- logger.error( { error: error, message: 'Error fetching revop counts', function: 'footfallDirectoryTicket-ticketCreation' } );
359
- // Continue with default 0 values if query fails
360
- }
355
+ }
356
+ } );
361
357
  }
362
358
 
363
- logger.info( { getCategory: getCategory } );
364
359
 
365
360
  // Calculate revisedFootfall: footfallCount - (sum of all counts)
366
- const totalCount = ( getCategory.duplicateCount || 0 ) +
367
- ( getCategory.employeeCount || 0 ) +
368
- ( getCategory.houseKeepingCount || 0 ) +
369
- ( getCategory.junkCount || 0 );
361
+
362
+ const totalCount = Array.isArray( tempAcc ) ?
363
+ tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
364
+ 0;
370
365
  const footfallCount = hits?.[0]?._source?.footfall_count || 0;
371
- logger.info( { footfallCount, totalCount } );
372
366
  const revisedFootfall = Math.max( 0, footfallCount - totalCount );
373
367
  if ( footfallCount - revisedFootfall == 0 ) {
374
368
  return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
@@ -400,7 +394,6 @@ export async function ticketCreation( req, res, next ) {
400
394
  return res.sendError( 'You don’t have any tagged images right now', 400 );
401
395
  }
402
396
  const formattedTaggingData = formatRevopTaggingHits( taggingImages );
403
- logger.info( { revopTaggingData: formattedTaggingData } );
404
397
 
405
398
  const record = {
406
399
  storeId: inputData.storeId,
@@ -420,12 +413,13 @@ export async function ticketCreation( req, res, next ) {
420
413
  type: 'tagging',
421
414
  mode: inputData.mode,
422
415
  revicedFootfall: revisedFootfall,
423
- count: getCategory,
416
+ count: tempAcc,
424
417
  revisedDetail: formattedTaggingData,
425
418
  status: 'raised',
426
419
  createdByEmail: req?.user?.email,
427
420
  createdByUserName: req?.user?.userName,
428
421
  createdByRole: req?.user?.role,
422
+ createdAt: new Date(),
429
423
  },
430
424
  ],
431
425
  };