tango-app-api-audit 3.6.57 → 3.6.59

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-audit",
3
- "version": "3.6.57",
3
+ "version": "3.6.59",
4
4
  "description": "audit & audit metrics apis",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "nodemailer": "^7.0.10",
29
29
  "nodemon": "^3.1.3",
30
30
  "swagger-ui-express": "^5.0.1",
31
- "tango-api-schema": "^2.5.27",
31
+ "tango-api-schema": "^2.6.42",
32
32
  "tango-app-api-middleware": "^3.6.18",
33
33
  "winston": "^3.13.0",
34
34
  "winston-daily-rotate-file": "^5.0.0"
@@ -73,6 +73,7 @@ import { aggregateTraxAuditData, findOneTraxAuditData } from '../service/traxAud
73
73
  import { bulkWriteAuditUserWallet, createAuditUserWallet, findOneAuditUserWallet } from '../service/auditUserWallet.service.js';
74
74
  import { updateOneAuditUsers } from '../service/auditUsers.service.js';
75
75
  import { bulkUpdate, clearScroll, upsertOpenSearchData, scrollResponse, searchOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
76
+
76
77
  // import { aggregateAuditUsers } from '../service/auditUsers.service.js';
77
78
 
78
79
  /* < -- *** Configuration *** --> */
@@ -937,7 +938,8 @@ export async function getFilterData( msg, user ) {
937
938
  ( item ) =>
938
939
  item.temp_ids < 20000 &&
939
940
  item.temp_ids > 0 &&
940
- item.person_status === '',
941
+ item.person_status === '' &&
942
+ item.query_status !== '',
941
943
  );
942
944
  const finalResult = await filterData.map( ( i ) => {
943
945
  return { ...i, img: i.index.concat( '_', i.temp_ids ) };
@@ -1084,9 +1086,14 @@ export async function workSpace( req, res ) {
1084
1086
  const dateRange = await getDate( new Date(), new Date() );
1085
1087
  const filter = [
1086
1088
  { status: { $in: [ 'active', 'hold' ] } },
1087
- { 'auditConfigs.audit': { $eq: true } },
1088
1089
  { clientId: { $in: inputData.clientId } },
1089
1090
  ];
1091
+ if ( [ 'traffic', 'zone' ].includes( inputData?.moduleType ) ) {
1092
+ filter.push( { 'auditConfigs.audit': { $eq: true } } );
1093
+ }
1094
+ if ( inputData?.moduleType === 'track' ) {
1095
+ filter.push( { 'auditConfigs.trackAudit': { $eq: true } } );
1096
+ }
1090
1097
 
1091
1098
  const temp = [];
1092
1099
  const clientQuery = [
@@ -1595,6 +1602,17 @@ export async function save( req, res ) {
1595
1602
  moduleType: inputData.moduleType,
1596
1603
  zoneName: inputData.zoneName,
1597
1604
  } );
1605
+ if ( storeAuditData?.status === 'completed' ) {
1606
+ logger.info( {
1607
+ message: 'This file is already submitted',
1608
+ storeId: inputData.storeId,
1609
+ fileDate: inputData.fileDate,
1610
+ moduleType: inputData.moduleType,
1611
+ zoneName: inputData.zoneName,
1612
+ function: 'save',
1613
+ } );
1614
+ return res.sendError( 'This file is already submitted', 409 );
1615
+ }
1598
1616
  if ( inputData.moduleType !=='track' ) {
1599
1617
  const params = {
1600
1618
  Key: `${inputData.storeId}/${inputData.zoneName}/${inputData.fileDate}/${bucket.masterJsonFile}`,
@@ -2560,26 +2578,31 @@ export async function clientMetrics( req, res ) {
2560
2578
  { moduleType: { $eq: inputData.moduleType } },
2561
2579
  ];
2562
2580
 
2563
- let storeAuditFilter = [
2564
- { $eq: [ '$clientId', '$$clientId' ] },
2565
- { $eq: [ '$fileDate', '$$fileDate' ] },
2566
- {
2567
- $eq: [ '$moduleType', inputData.moduleType ],
2568
- },
2569
-
2570
- ];
2571
-
2572
2581
  if ( inputData?.filterByClient?.length > 0 ) {
2573
2582
  filter.push(
2574
2583
  { clientId: { $in: inputData.filterByClient } },
2575
2584
  );
2576
2585
  }
2577
- const query = [
2578
- {
2579
- $match: {
2580
- $and: filter,
2581
- },
2582
- },
2586
+
2587
+ // Only show clients enabled for audit ("access" clients): traffic/zone use
2588
+ // auditConfigs.audit, track uses auditConfigs.trackAudit.
2589
+ const clientAccessFilter = [];
2590
+ if ( [ 'traffic', 'zone' ].includes( inputData?.moduleType ) ) {
2591
+ clientAccessFilter.push( { 'auditConfigs.audit': { $eq: true } } );
2592
+ }
2593
+ if ( inputData?.moduleType === 'track' ) {
2594
+ clientAccessFilter.push( { 'auditConfigs.trackAudit': { $eq: true } } );
2595
+ }
2596
+ if ( clientAccessFilter.length > 0 ) {
2597
+ const accessClients = await aggregateClient( [
2598
+ { $match: { $and: clientAccessFilter } },
2599
+ { $project: { _id: 0, clientId: 1 } },
2600
+ ] );
2601
+ filter.push( { clientId: { $in: accessClients.map( ( c ) => c.clientId ) } } );
2602
+ }
2603
+ // --- Base metrics from auditStoreData (one indexed aggregation) --------
2604
+ const baseQuery = [
2605
+ { $match: { $and: filter } },
2583
2606
  {
2584
2607
  $group: {
2585
2608
  _id: { clientId: '$clientId', fileDate: '$fileDate', moduleType: '$moduleType' },
@@ -2592,181 +2615,125 @@ export async function clientMetrics( req, res ) {
2592
2615
  totalFilesCount: { $sum: 1 },
2593
2616
  },
2594
2617
  },
2618
+ ];
2619
+
2620
+ // --- Store status counts from storeAudit (one indexed aggregation) -----
2621
+ // Replaces the old correlated per-group $lookup: instead of running a
2622
+ // sub-query for every client/date group, aggregate storeAudit once over the
2623
+ // same filter and merge by clientId+fileDate in JS below. storeAudit has the
2624
+ // same clientId / moduleType / fileDateISO fields, so `filter` applies as-is.
2625
+ const storeAuditQuery = [
2626
+ { $match: { $and: filter } },
2595
2627
  {
2596
- $project: {
2597
- fileDate: 1,
2598
- clientId: 1,
2599
- clientName: 1,
2600
- queueName: 1,
2601
- installedStore: 1,
2602
- totalFilesCount: 1,
2603
- moduleType: 1,
2604
- notAssignedCount: { $ifNull: [ 0, 0 ] },
2605
- clientStatus: { $ifNull: [ '', '' ] },
2628
+ $group: {
2629
+ _id: { clientId: '$clientId', fileDate: '$fileDate' },
2630
+ clientId: { $first: '$clientId' },
2631
+ fileDate: { $first: '$fileDate' },
2632
+ completedStores: { $sum: { $cond: [ { $eq: [ '$status', 'completed' ] }, 1, 0 ] } },
2633
+ notAssignedStores: { $sum: { $cond: [ { $eq: [ '$status', 'not_assign' ] }, 1, 0 ] } },
2634
+ inprogressStores: { $sum: { $cond: [ { $in: [ '$status', [ 'inprogress', 'drafted', 'assigned' ] ] }, 1, 0 ] } },
2635
+ fileCount: { $sum: 1 },
2606
2636
  },
2607
2637
  },
2608
- {
2609
- $lookup: {
2610
- 'from': 'storeAudit',
2611
- 'let': { clientId: '$clientId', fileDate: '$fileDate', totalAuditFiles: '$totalFilesCount' },
2612
- 'pipeline': [
2638
+ ];
2613
2639
 
2614
- {
2615
- $match: {
2616
- $expr: {
2617
- $and: storeAuditFilter,
2618
- },
2619
- },
2620
- },
2621
- {
2622
- $project: {
2623
- completedStores: { $cond: [ { $eq: [ '$status', 'completed' ] }, 1, 0 ] },
2624
- notAssignedStores: { $cond: [ { $eq: [ '$status', 'not_assign' ] }, 1, 0 ] },
2625
- inprogressStores: { $cond: [ { $in: [ '$status', [ 'inprogress', 'drafted', 'assigned' ] ] }, 1, 0 ] },
2626
- },
2627
- },
2628
- {
2629
- $group: {
2630
- _id: { clientId: '$clientId', fileDate: '$fileDate' },
2631
- completedStores: { $sum: '$completedStores' },
2632
- inprogressStores: { $sum: '$inprogressStores' },
2633
- notAssignedStores: { $sum: '$notAssignedStores' },
2634
- fileCount: { $sum: 1 },
2635
- },
2636
- },
2637
- {
2638
- $project: {
2639
- _id: 0,
2640
- completedStores: 1,
2641
- inprogressStores: 1,
2642
- notAssignedStores: 1,
2643
- fileCount: { $ifNull: [ '$fileCount', 0 ] },
2644
- completionPercentage: {
2645
- $ifNull: [
2646
- {
2647
- $round: [ {
2648
- $multiply: [
2649
- 100, {
2650
- $divide: [
2651
- '$completedStores', '$$totalAuditFiles',
2652
- ],
2653
- },
2654
- ],
2655
- }, 1 ],
2640
+ const [ baseRows, storeRows ] = await Promise.all( [
2641
+ aggregateAuditStoreData( baseQuery ),
2642
+ aggregateStoreAudit( storeAuditQuery ),
2643
+ ] );
2656
2644
 
2657
- },
2658
- 0,
2659
- ],
2660
- },
2661
- },
2662
- },
2663
- ], 'as': 'storeAudit',
2664
- },
2665
- },
2666
- {
2667
- $unwind: {
2668
- path: '$storeAudit',
2669
- preserveNullAndEmptyArrays: true,
2670
- },
2671
- },
2672
- {
2673
- $project: {
2674
- _id: 0,
2675
- fileDate: 1,
2676
- clientId: 1,
2677
- clientName: 1,
2678
- totalFilesCount: 1,
2679
- moduleType: 1,
2680
- installedStore: 1,
2681
- // notAssignedStores: '$storeAudit.notAssignedStores',
2682
- inprogressStoresCount: '$storeAudit.inprogressStores',
2683
- // fileCount: '$storeAudit.fileCount',
2684
- notAssignedCount: { $subtract: [ '$totalFilesCount', { $subtract: [ { $ifNull: [ '$storeAudit.fileCount', 0 ] }, { $ifNull: [ '$storeAudit.notAssignedStores', 0 ] } ] } ] },
2685
- clientStatus: { $cond: [ { $eq: [ { $ifNull: [ '$storeAudit.fileCount', 0 ] }, 0 ] }, 'not-taken', { $cond: [ { $gte: [ '$storeAudit.completedStores', '$totalFilesCount' ] }, 'completed', 'pending' ] } ] }, // { $cond: [ { $or: [ { $gt: [ '$notAssignedCount', 0 ] }, { $gt: [ '$storeAudit.notAssignedStores', 0 ] } ] }
2686
- completedStores: '$storeAudit.completedStores',
2687
- completionPercentage: '$storeAudit.completionPercentage',
2645
+ // Index storeAudit counts by clientId + fileDate for an O(1) merge.
2646
+ const storeByKey = new Map();
2647
+ for ( const s of storeRows ) {
2648
+ storeByKey.set( `${s.clientId}__${s.fileDate}`, s );
2649
+ }
2688
2650
 
2689
- },
2690
- },
2691
- ];
2651
+ // Merge and derive the same fields the old $lookup / $project produced.
2652
+ let merged = baseRows.map( ( base ) => {
2653
+ const sa = storeByKey.get( `${base.clientId}__${base.fileDate}` );
2654
+ const fileCount = sa?.fileCount || 0;
2655
+ const notAssignedStores = sa?.notAssignedStores || 0;
2656
+ const completedStores = sa ? sa.completedStores : undefined;
2657
+ const clientStatus = fileCount === 0 ?
2658
+ 'not-taken' :
2659
+ ( ( completedStores || 0 ) >= base.totalFilesCount ? 'completed' : 'pending' );
2660
+ return {
2661
+ fileDate: base.fileDate,
2662
+ clientId: base.clientId,
2663
+ clientName: base.clientName,
2664
+ totalFilesCount: base.totalFilesCount,
2665
+ moduleType: base.moduleType,
2666
+ installedStore: base.installedStore,
2667
+ inprogressStoresCount: sa ? sa.inprogressStores : undefined,
2668
+ notAssignedCount: base.totalFilesCount - ( fileCount - notAssignedStores ),
2669
+ clientStatus: clientStatus,
2670
+ completedStores: completedStores,
2671
+ completionPercentage: sa ?
2672
+ Math.round( ( ( 100 * sa.completedStores ) / base.totalFilesCount ) * 10 ) / 10 :
2673
+ undefined,
2674
+ };
2675
+ } );
2692
2676
 
2677
+ // --- Post-merge filters (depend on the derived clientStatus) -----------
2693
2678
  if ( inputData?.filterByStatus?.length > 0 ) {
2694
- query.push(
2695
- {
2696
- $match: { clientStatus: { $in: inputData.filterByStatus } },
2697
- },
2698
-
2699
- );
2679
+ merged = merged.filter( ( r ) => inputData.filterByStatus.includes( r.clientStatus ) );
2700
2680
  }
2701
2681
 
2702
2682
  if ( inputData.searchValue && inputData.searchValue !== '' ) {
2703
- query.push( {
2704
- $match: {
2705
- $or: [
2706
- { clientId: { $regex: inputData.searchValue, $options: 'i' } },
2707
- { clientName: { $regex: inputData.searchValue, $options: 'i' } },
2708
- { moduleType: { $regex: inputData.searchValue, $options: 'i' } },
2709
- { clientStatus: { $regex: inputData.searchValue, $options: 'i' } },
2710
- ],
2711
-
2712
- },
2713
- } );
2683
+ const rx = new RegExp( inputData.searchValue, 'i' );
2684
+ merged = merged.filter( ( r ) =>
2685
+ rx.test( r.clientId || '' ) ||
2686
+ rx.test( r.clientName || '' ) ||
2687
+ rx.test( r.moduleType || '' ) ||
2688
+ rx.test( r.clientStatus || '' ),
2689
+ );
2714
2690
  }
2691
+
2692
+ // --- Sort: numeric columns numerically, strings case-insensitively to
2693
+ // match the DB collation ({ locale: 'en', strength: 2 }). --------------
2715
2694
  if ( inputData.sortColumnName ) {
2716
- const sortBy = inputData.sortBy || -1;
2717
- const sortColumn = inputData.sortColumnName;
2718
- query.push( {
2719
- $sort: { [sortColumn]: sortBy },
2720
- },
2721
- );
2695
+ const dir = Number( inputData.sortBy ) === 1 ? 1 : -1;
2696
+ const col = inputData.sortColumnName;
2697
+ merged.sort( ( a, b ) => {
2698
+ const av = a[col];
2699
+ const bv = b[col];
2700
+ let cmp;
2701
+ if ( typeof av === 'number' && typeof bv === 'number' ) {
2702
+ cmp = av - bv;
2703
+ } else {
2704
+ cmp = String( av ?? '' ).localeCompare( String( bv ?? '' ), 'en', { sensitivity: 'accent' } );
2705
+ }
2706
+ return dir * cmp;
2707
+ } );
2722
2708
  }
2723
2709
 
2724
- const count = await aggregateAuditStoreData( query );
2725
- if ( count.length == 0 ) {
2710
+ const count = merged.length;
2711
+ if ( count === 0 ) {
2726
2712
  return res.sendError( 'No Data Found', 204 );
2727
2713
  }
2728
2714
 
2729
2715
  if ( inputData.isExport ) {
2730
- query.push( { $limit: 10000 } );
2731
- } else {
2732
- query.push( {
2733
- $skip: offset,
2734
- },
2735
- {
2736
- $limit: limit,
2737
- } );
2738
- }
2739
- if ( inputData.isExport ) {
2740
- query.push(
2741
- {
2742
- $project: {
2743
- '_id': 0,
2744
- 'File Date': '$fileDate',
2745
- 'Brand Name': '$clientName',
2746
- 'Brand Id': '$clientId',
2747
- 'Product Type': '$moduleType',
2748
- // 'Completed Percentage': '$completionPercentage',
2749
- 'Installed Stores': '$installedStore',
2750
- 'Audit Stores': '$totalFilesCount',
2751
- 'Inprogress Stores': '$inprogressStoresCount',
2752
- 'Not Assigned stores': '$notAssignedCount',
2753
- // 'Not Assigned Stores': '$notAssignedStores',
2754
- 'Completed Stores': '$completedStores',
2755
- 'Status': '$clientStatus',
2756
- },
2757
- },
2758
- );
2716
+ const exportRows = merged.slice( 0, 10000 ).map( ( r ) => ( {
2717
+ 'File Date': r.fileDate,
2718
+ 'Brand Name': r.clientName,
2719
+ 'Brand Id': r.clientId,
2720
+ 'Product Type': r.moduleType,
2721
+ 'Installed Stores': r.installedStore,
2722
+ 'Audit Stores': r.totalFilesCount,
2723
+ 'Inprogress Stores': r.inprogressStoresCount,
2724
+ 'Not Assigned stores': r.notAssignedCount,
2725
+ 'Completed Stores': r.completedStores,
2726
+ 'Status': r.clientStatus,
2727
+ } ) );
2728
+ await download( exportRows, res );
2729
+ return;
2759
2730
  }
2760
2731
 
2761
- const result = await aggregateAuditStoreData( query );
2762
- if ( result.length == 0 ) {
2732
+ const pageResult = merged.slice( offset, offset + limit );
2733
+ if ( pageResult.length === 0 ) {
2763
2734
  return res.sendError( 'No Data Found', 204 );
2764
2735
  }
2765
- if ( inputData.isExport ) {
2766
- await download( result, res );
2767
- return;
2768
- }
2769
- return res.sendSuccess( { result: result, count: count.length } );
2736
+ return res.sendSuccess( { result: pageResult, count: count } );
2770
2737
  } catch ( error ) {
2771
2738
  const err = error.error || 'Internal Server Error';
2772
2739
  logger.error( { error: error, message: req.body, function: 'clientMetrics' } );
@@ -3715,6 +3682,13 @@ export async function reTrigger( req, res ) {
3715
3682
  }
3716
3683
  switch ( inputData.triggerType ) {
3717
3684
  case 'queue':
3685
+ // Only push to the queue when the store has not already been queued.
3686
+ // A 'not_assign' status means a prior reTrigger already pushed this
3687
+ // store - pushing again would create a duplicate queue message.
3688
+ if ( req.audit.status === 'not_assign' ) {
3689
+ await releaseReTriggerLock( lockKey );
3690
+ return res.sendError( 'This store has already been pushed to the queue', 400 );
3691
+ }
3718
3692
  // Only push to the queue when the store has not already been queued.
3719
3693
  // A 'not_assign' status means a prior reTrigger already pushed this
3720
3694
  // store - pushing again would create a duplicate queue message.
@@ -5899,3 +5873,5 @@ export async function rePushAuditStores( req, res ) {
5899
5873
  return res.sendError( err, 500 );
5900
5874
  }
5901
5875
  }
5876
+
5877
+
@@ -1414,7 +1414,6 @@ export async function summaryList( req, res ) {
1414
1414
  if ( inputData.clientId !== '11' ) {
1415
1415
  return res.sendError( 'No data found', 204 );
1416
1416
  }
1417
-
1418
1417
  // declare openSearch and get data form the .env/secret manager
1419
1418
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1420
1419
 
@@ -1764,6 +1763,7 @@ export async function summaryList( req, res ) {
1764
1763
  } );
1765
1764
  }
1766
1765
  const getcount = await aggregateClusters( inputData.keyType === 'rm'?RMQuery: clusterQuery );
1766
+
1767
1767
  const count = getcount?.length || 0;
1768
1768
  if ( count == 0 ) {
1769
1769
  return res.sendError( 'No data', 204 );
@@ -1787,6 +1787,7 @@ export async function summaryList( req, res ) {
1787
1787
  );
1788
1788
  }
1789
1789
  const getCluster = await aggregateClusters( inputData.keyType === 'rm'?RMQuery: clusterQuery );
1790
+ logger.info( { masg: '...........getCluster', getCluster, RMQuery } );
1790
1791
  if ( !getCluster || getCluster?.length == 0 ) {
1791
1792
  return res.sendError( 'No data', 204 );
1792
1793
  }
@@ -3174,3 +3175,175 @@ export async function updateEmailConfig1( req, res ) {
3174
3175
  return res.sendError( err, 500 );
3175
3176
  }
3176
3177
  }
3178
+
3179
+ export async function getConversationDetails( req, res ) {
3180
+ try {
3181
+ const inputData = req.params;
3182
+
3183
+ // Query OpenSearch audio_analytics_report index by doc_id and moduleType
3184
+ const query = {
3185
+ 'query': {
3186
+ 'bool': {
3187
+ 'must': [
3188
+ {
3189
+ 'term': {
3190
+ 'unique_id.keyword': inputData.id,
3191
+ },
3192
+ },
3193
+ ],
3194
+ },
3195
+ },
3196
+ 'size': 1,
3197
+ };
3198
+
3199
+ const result = await getOpenSearchData( 'audio_analytics_report_v2', query );
3200
+ if ( result?.body?.hits?.hits?.length === 0 ) {
3201
+ return res.sendError( 'No data found', 204 );
3202
+ }
3203
+
3204
+ const source = result?.body?.hits?.hits[0]?._source;
3205
+
3206
+ if ( !source ) {
3207
+ return res.sendError( 'No data found', 204 );
3208
+ }
3209
+
3210
+ // Helper function to format duration: convert seconds to mm:ss format
3211
+ const formatDuration = ( seconds ) => {
3212
+ if ( !seconds || seconds === 0 ) return '0:00';
3213
+ const minutes = Math.floor( seconds / 60 );
3214
+ const secs = Math.floor( seconds % 60 );
3215
+ return `${minutes}:${secs.toString().padStart( 2, '0' )}`;
3216
+ };
3217
+
3218
+ // Helper: parse "s3://bucket/path/to/file" into { Bucket, file_path }
3219
+ const parseS3Uri = ( uri ) => {
3220
+ if ( !uri ) return null;
3221
+ const withoutScheme = uri.replace( /^s3:\/\//, '' );
3222
+ const slashIdx = withoutScheme.indexOf( '/' );
3223
+ if ( slashIdx === -1 ) return null;
3224
+ return {
3225
+ Bucket: withoutScheme.slice( 0, slashIdx ),
3226
+ file_path: withoutScheme.slice( slashIdx + 1 ),
3227
+ };
3228
+ };
3229
+
3230
+ // Helper function to format transcript: fetch and clean transcript file
3231
+ const getFormattedTranscript = async ( transcriptPath ) => {
3232
+ try {
3233
+ if ( !transcriptPath ) return '';
3234
+
3235
+ const s3Parts = parseS3Uri( transcriptPath );
3236
+ logger.info( { s3Parts: s3Parts, function: 'getFormattedTranscript' } );
3237
+ if ( !s3Parts ) return '';
3238
+
3239
+ const transcriptSignedUrl = await signedUrl( { ...s3Parts, key: 'aws' } );
3240
+ if ( !transcriptSignedUrl ) return '';
3241
+
3242
+ const fetchRes = await fetch( transcriptSignedUrl );
3243
+ if ( !fetchRes.ok ) return '';
3244
+ let text = await fetchRes.text();
3245
+
3246
+ // Remove timestamps like [00:00:33.954 --> 00:00:36.154]
3247
+ text = text.replace( /\[\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3}\]\s*/g, '' );
3248
+
3249
+ // Remove [silence] markers
3250
+ text = text.replace( /\[silence\]\s*/g, '' );
3251
+
3252
+ // Replace multiple newlines with single space to create paragraphs
3253
+ text = text.replace( /\n+/g, ' ' );
3254
+
3255
+ // Clean up multiple spaces
3256
+ text = text.replace( /\s+/g, ' ' ).trim();
3257
+
3258
+ return text;
3259
+ } catch ( error ) {
3260
+ logger.error( { message: error, function: 'getFormattedTranscript' } );
3261
+ return '';
3262
+ }
3263
+ };
3264
+
3265
+ // Helper function to format metric names
3266
+ const formatMetricName = ( name ) => {
3267
+ const nameMap = {
3268
+ 'Torchlight Exam': 'Torchlight Check',
3269
+ 'JCC': 'JCC Check',
3270
+ 'Distance Visual Acuity': 'Distance-VA-Check',
3271
+ 'Duochrome': 'Duochrome Check',
3272
+ 'Near Visual Acuity': 'Near Addition Check',
3273
+ };
3274
+ return nameMap[name] || name;
3275
+ };
3276
+
3277
+ // Format metrics results with sequential time allocation
3278
+ const matchedCohort = source?.cohort_analysis_results?.find( ( c ) => c.cohortId === 'cohort_11_1ddea945-ff3a-4c11-95a9-bb425ca927de' );
3279
+ const metricsData = matchedCohort?.metrics_results || [];
3280
+
3281
+ // Calculate sequential times for detected metrics
3282
+ let currentSeconds = 0;
3283
+ const totalDuration = Math.floor( source?.duration || 1200 );
3284
+ const detectedCount = metricsData.filter( ( m ) => m.contextsEvaluated?.[0]?.isDetected ).length || 1;
3285
+ const secondsPerMetric = Math.floor( totalDuration / Math.max( detectedCount, 1 ) );
3286
+
3287
+ const formattedMetrics = metricsData.map( ( metric, index ) => {
3288
+ const isDetected = metric.contextsEvaluated?.[0]?.isDetected;
3289
+
3290
+ let startTime = '';
3291
+ let endTime = '';
3292
+
3293
+ if ( isDetected ) {
3294
+ const startSeconds = currentSeconds;
3295
+ const endSeconds = Math.min( currentSeconds + secondsPerMetric, totalDuration );
3296
+
3297
+ startTime = formatDuration( startSeconds );
3298
+ endTime = formatDuration( endSeconds );
3299
+
3300
+ currentSeconds = endSeconds;
3301
+ }
3302
+
3303
+ return {
3304
+ metricId: metric.metricId,
3305
+ metricName: formatMetricName( metric.metricName ),
3306
+ metricType: metric.metricType,
3307
+ status: metric.contextsEvaluated?.[0]?.isDetected === false ? 'fail':'pass',
3308
+ contextsEvaluated: metric.contextsEvaluated?.[0]?.isDetected,
3309
+ startTime: startTime,
3310
+ endTime: endTime,
3311
+ evidence_quote: metric.contextsEvaluated?.[0]?.isDetected === false ? '' : metric.evidence_quote,
3312
+ };
3313
+ } ) || [];
3314
+
3315
+ // Fetch signed URL for audio
3316
+ const audioS3Parts = parseS3Uri( source?.s3_audio_path );
3317
+ const audioSignedUrl = audioS3Parts ? await signedUrl( { ...audioS3Parts, key: 'aws' } ) : null;
3318
+ // Fetch and format transcript
3319
+ const formattedTranscript = await getFormattedTranscript( source?.s3_transcript_path );
3320
+
3321
+ // Get executive summary
3322
+ // const executiveSummary = source?.cohort_analysis_results?.[0]?.executive_summary || {};
3323
+
3324
+ const response = {
3325
+ doc_id: source?.doc_id,
3326
+ // storeName: source?.storeName,
3327
+ // storeID: source?.storeID,
3328
+ // staffID: source?.staffID,
3329
+ // state: source?.state,
3330
+ // moduleType: source?.moduleType,
3331
+ // fileDate: source?.filedate,
3332
+ // processTimestamp: source?.processTimestamp,
3333
+ duration: formatDuration( source?.duration ),
3334
+ audio_url: audioSignedUrl,
3335
+ // s3_transcript_path: source?.s3_transcript_path,
3336
+ transcript_summary: formattedTranscript,
3337
+ // overall_score: executiveSummary?.overall_score,
3338
+ // conversation_summary: executiveSummary?.conversation_summary,
3339
+ // key_pointers: executiveSummary?.key_pointers,
3340
+ metrics_results: formattedMetrics,
3341
+ };
3342
+
3343
+ return res.sendSuccess( { result: response } );
3344
+ } catch ( error ) {
3345
+ const err = error.message || 'Internal Server Error';
3346
+ logger.error( { message: error, data: req.params, function: 'eyetestAudit-controller-getConversationDetails' } );
3347
+ return res.sendError( err, 500 );
3348
+ }
3349
+ }
@@ -321,3 +321,5 @@ export const rePushAuditStoresSchema = joi.object( {
321
321
  export const rePushAuditStoresValid = {
322
322
  body: rePushAuditStoresSchema,
323
323
  };
324
+
325
+
@@ -418,3 +418,14 @@ export const getEmailConfigSchema = joi.object( {
418
418
  export const getEmailConfigValid = {
419
419
  query: getEmailConfigSchema,
420
420
  };
421
+
422
+ export const getConversationDetailsSchema = joi.object( {
423
+ id: joi.string().required().messages( {
424
+ 'any.required': 'ID is required',
425
+ 'string.empty': 'ID cannot be empty',
426
+ } ),
427
+ } );
428
+
429
+ export const getConversationDetailsValid = {
430
+ params: getConversationDetailsSchema,
431
+ };
@@ -45,7 +45,10 @@ auditRouter.get( '/get-user-audit-count-mtd', isAllowedSessionHandler, getUserAu
45
45
 
46
46
  // audit log
47
47
  auditRouter.get( '/audit-view-logs', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ] } ), validate( auditViewLogValid ), auditViewLogs );
48
+
49
+
48
50
  export default auditRouter;
49
51
 
50
52
  //
51
53
  auditRouter.post( '/re-push-audit-stores', validate( rePushAuditStoresValid ), rePushAuditStores );
54
+
@@ -1,8 +1,8 @@
1
1
  import { Router } from 'express';
2
2
  import { isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
3
3
  import { accessVerification } from 'tango-app-api-middleware';
4
- import { cancelValid, clusterListValid, emailAlertLogValid, getEmailConfigValid, getFileHistoryValid, getFileValid, getListValid, getUserConfigValid, rmListValid, saveValid, summaryCardValid, summaryListValid, updateEmailConfigValid, updateUserConfigValid, userAuditedDataValid, viewFileValid } from '../dtos/eyeTestAudit.dtos.js';
5
- import { addUserConfig, cancel, clusterList, emailAlertLog, getEmailConfig, getFile, getFileHistory, getList, getUserConfig, rmList, save, summaryCard, summaryList, updateEmailConfig, updateEmailConfig1, updateUserConfig, userAuditedData, viewFile } from '../controllers/eyeTestAudit.controllers.js';
4
+ import { cancelValid, clusterListValid, emailAlertLogValid, getConversationDetailsValid, getEmailConfigValid, getFileHistoryValid, getFileValid, getListValid, getUserConfigValid, rmListValid, saveValid, summaryCardValid, summaryListValid, updateEmailConfigValid, updateUserConfigValid, userAuditedDataValid, viewFileValid } from '../dtos/eyeTestAudit.dtos.js';
5
+ import { addUserConfig, cancel, clusterList, emailAlertLog, getConversationDetails, getEmailConfig, getFile, getFileHistory, getList, getUserConfig, rmList, save, summaryCard, summaryList, updateEmailConfig, updateEmailConfig1, updateUserConfig, userAuditedData, viewFile } from '../controllers/eyeTestAudit.controllers.js';
6
6
  import { isSuperAdmin, roleValidation, sendAlert } from '../validation/eyeTest.validation.js';
7
7
 
8
8
  export const eyeTestAuditRouter = Router();
@@ -15,6 +15,10 @@ eyeTestAuditRouter.post( '/cancel', isAllowedSessionHandler, accessVerification(
15
15
  eyeTestAuditRouter.get( '/get-file-history/:id/:type', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getFileHistoryValid ), getFileHistory );
16
16
  eyeTestAuditRouter.get( '/user-audited-data/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( userAuditedDataValid ), userAuditedData );
17
17
 
18
+ // enhancement of march of 2026
19
+
20
+ eyeTestAuditRouter.get( '/get-conversation-details/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getConversationDetailsValid ), getConversationDetails );
21
+
18
22
  // enhancement of aug of 2025
19
23
 
20
24
  eyeTestAuditRouter.post( '/summary-list', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( summaryListValid ), roleValidation, summaryList );
@@ -151,13 +151,11 @@ export async function mapCustomer( sourceMappingData, customerData ) {
151
151
  try {
152
152
  const filterCustomerData = await filteredCustomerMap( customerData );
153
153
  logger.info( { filterCustomerData: filterCustomerData } );
154
- const chunkedMappingData = await chunkArray( sourceMappingData, 10 );
155
-
156
- const promises = chunkedMappingData.map( async ( chunk ) => {
157
- return mapFunction( chunk, filterCustomerData );
158
- } );
159
- const mappedArrays = await Promise.all( promises );
160
- return mappedArrays.flat();
154
+ // Build the lookup once, then retag every row in a single pass. This is
155
+ // O(rows + mappedIds) and scales for concurrent submissions, versus the old
156
+ // O(customers x mappedIds x rows) scan that also re-read its own mutations.
157
+ const customerMap = buildCustomerMap( filterCustomerData );
158
+ return mapFunction( sourceMappingData, customerMap );
161
159
  } catch ( error ) {
162
160
  logger.error( { error: error, type: 'mapCustomer' } );
163
161
  return error;
@@ -168,22 +166,39 @@ const filteredCustomerMap = async ( data ) => {
168
166
  return data.filter( ( map ) => map.count >= 1 );
169
167
  };
170
168
 
171
- const mapFunction = async ( chunkData, filterData ) => {
169
+ // Lookup of original cluster id (child img_name) -> the customer it was tagged
170
+ // into. Last write wins if the same id is referenced by more than one group, so
171
+ // the result is deterministic regardless of customer order.
172
+ const buildCustomerMap = ( filterData ) => {
173
+ const customerMap = new Map();
172
174
  for ( const data of filterData ) {
173
- const mappedIds = data.mappedid;
174
- if ( data?.mappedid ) {
175
- for ( const mappedId of mappedIds ) {
176
- chunkData.find( ( item ) => {
177
- if ( mappedId.img_name === item.temp_ids ) {
178
- item.temp_ids = data.img_name;
179
- ( mappedId.img_name !== data.img_name ) ? item.query_status = '' : null;
180
- item.demographic = data.demographic || '';
181
- }
182
- } );
183
- }
175
+ if ( !data?.mappedid ) continue;
176
+ for ( const mappedId of data.mappedid ) {
177
+ customerMap.set( mappedId.img_name, {
178
+ parentId: data.img_name,
179
+ demographic: data.demographic || '',
180
+ // The chosen representative (img_name === parent) keeps its query flag so
181
+ // it resurfaces in reaudit; every other merged image is cleared so it does
182
+ // not (query_status is the reaudit gate in getFilterData / getAuditFilterData).
183
+ isChild: mappedId.img_name !== data.img_name,
184
+ } );
184
185
  }
185
186
  }
186
- return chunkData;
187
+ return customerMap;
188
+ };
189
+
190
+ // Single pass over the master-json rows. Each row's ORIGINAL temp_ids is read
191
+ // exactly once and reassigned to its customer's parent id, so every row sharing
192
+ // a temp_ids is retagged and the retag never feeds back into later lookups.
193
+ const mapFunction = ( personData, customerMap ) => {
194
+ for ( const item of personData ) {
195
+ const mapped = customerMap.get( item.temp_ids );
196
+ if ( !mapped ) continue;
197
+ item.temp_ids = mapped.parentId;
198
+ item.query_status = mapped.isChild ? '' : item.query_status;
199
+ item.demographic = mapped.demographic;
200
+ }
201
+ return personData;
187
202
  };
188
203
 
189
204
  export async function isAuditInputFolderExist( req, res, next ) {
@@ -345,7 +360,8 @@ export async function getAuditFilterData( data ) {
345
360
  ( item ) =>
346
361
  item.temp_ids < 20000 &&
347
362
  item.temp_ids > 0 &&
348
- item.person_status === '',
363
+ item.person_status === '' &&
364
+ item.query_status !== '',
349
365
  );
350
366
  const finalResult = await filterData.map( ( i ) => {
351
367
  return { ...i, img: i.index.concat( '_', i.temp_ids ) };