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 +2 -2
- package/src/controllers/audit.controllers.js +145 -169
- package/src/controllers/eyeTestAudit.controllers.js +174 -1
- package/src/dtos/audit.dtos.js +2 -0
- package/src/dtos/eyeTestAudit.dtos.js +11 -0
- package/src/routes/audit.routes.js +3 -0
- package/src/routes/eyeTestAudit.routes.js +6 -2
- package/src/validation/audit.validation.js +37 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-audit",
|
|
3
|
-
"version": "3.6.
|
|
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.
|
|
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
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
$
|
|
2597
|
-
fileDate:
|
|
2598
|
-
clientId:
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
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 =
|
|
2725
|
-
if ( count
|
|
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
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
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
|
|
2762
|
-
if (
|
|
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
|
-
|
|
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
|
+
}
|
package/src/dtos/audit.dtos.js
CHANGED
|
@@ -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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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 ) };
|