tango-app-api-audit 3.6.25 → 3.6.26

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.25",
3
+ "version": "3.6.26",
4
4
  "description": "audit & audit metrics apis",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "author": "praveenraj",
14
14
  "license": "ISC",
15
15
  "dependencies": {
16
+ "@aws-sdk/client-ses": "^3.918.0",
16
17
  "adm-zip": "^0.5.15",
17
18
  "aws-sdk": "^2.1643.0",
18
19
  "axios": "^1.7.9",
@@ -24,6 +25,7 @@
24
25
  "joi-to-swagger": "^6.2.0",
25
26
  "lodash": "^4.17.21",
26
27
  "mongodb": "^6.7.0",
28
+ "nodemailer": "^7.0.10",
27
29
  "nodemon": "^3.1.3",
28
30
  "swagger-ui-express": "^5.0.1",
29
31
  "tango-api-schema": "^2.3.28",
@@ -609,7 +609,7 @@ export async function getAuditFile( req, res ) {
609
609
  const getQuery = msg.audit_type =='ReAudit'?
610
610
  {
611
611
  size: inputData?.limit || 500,
612
- _source: [ 'module', 'status', 'date', 'store_id', 'outputCluster', 'personPath', 'REIDCluster', 'isEmployee', 'isJunk', 'EmployeeStatusFinal', 'entry_time' ], // Only fetch necessary fields
612
+ _source: [ 'module', 'status', 'date', 'store_id', 'outputCluster', 'personPath', 'REIDCluster', 'isEmployee', 'isJunk', 'EmployeeStatusFinal' ], // Only fetch necessary fields
613
613
 
614
614
  query: {
615
615
  bool: {
@@ -638,13 +638,13 @@ export async function getAuditFile( req, res ) {
638
638
  },
639
639
  },
640
640
  sort: [
641
- { entry_time: { order: 'desc' } },
641
+ { timestamp: { order: 'desc' } },
642
642
  ],
643
643
 
644
644
  }:
645
645
  {
646
646
  size: inputData?.limit || 500,
647
- _source: [ 'module', 'status', 'date', 'store_id', 'personPath', 'REIDCluster', 'EmployeeStatusProposed', 'isJunk', 'EmployeeStatusFinal', 'entry_time' ], // Only fetch necessary fields
647
+ _source: [ 'module', 'status', 'date', 'store_id', 'personPath', 'REIDCluster', 'EmployeeStatusProposed', 'isJunk', 'EmployeeStatusFinal' ], // Only fetch necessary fields
648
648
 
649
649
  query: {
650
650
  bool: {
@@ -668,7 +668,7 @@ export async function getAuditFile( req, res ) {
668
668
  },
669
669
  },
670
670
  sort: [
671
- { entry_time: { order: 'desc' } },
671
+ { timestamp: { order: 'desc' } },
672
672
  ],
673
673
 
674
674
  };
@@ -4490,7 +4490,7 @@ export async function auditImages( req, res ) {
4490
4490
  case 'BC':
4491
4491
  const getQuery = {
4492
4492
  size: 10000,
4493
- _source: [ 'module', 'status', 'date', 'store_id', 'personPath', 'REIDCluster', 'EmployeeStatusProposed', 'isJunk', 'EmployeeStatusFinal', 'entry_time' ], // Only fetch necessary fields
4493
+ _source: [ 'module', 'status', 'date', 'store_id', 'personPath', 'REIDCluster', 'EmployeeStatusProposed', 'isJunk', 'EmployeeStatusFinal' ], // Only fetch necessary fields
4494
4494
 
4495
4495
  query: {
4496
4496
  bool: {
@@ -4515,7 +4515,7 @@ export async function auditImages( req, res ) {
4515
4515
  },
4516
4516
 
4517
4517
  sort: [
4518
- { entry_time: { order: 'desc' } },
4518
+ { timestamp: { order: 'desc' } },
4519
4519
  ],
4520
4520
  };
4521
4521
  const files = [];
@@ -4565,7 +4565,7 @@ export async function auditImages( req, res ) {
4565
4565
 
4566
4566
  const getACQuery = {
4567
4567
  size: 10000,
4568
- _source: [ 'module', 'status', 'date', 'store_id', 'outputCluster', 'personPath', 'REIDCluster', 'isEmployee', 'isJunk', 'EmployeeStatusFinal', 'entry_time' ], // Only fetch necessary fields
4568
+ _source: [ 'module', 'status', 'date', 'store_id', 'outputCluster', 'personPath', 'REIDCluster', 'isEmployee', 'isJunk', 'EmployeeStatusFinal' ], // Only fetch necessary fields
4569
4569
 
4570
4570
  query: {
4571
4571
  bool: {
@@ -4594,7 +4594,7 @@ export async function auditImages( req, res ) {
4594
4594
  },
4595
4595
  },
4596
4596
  sort: [
4597
- { entry_time: { order: 'desc' } },
4597
+ { timestamp: { order: 'desc' } },
4598
4598
  ],
4599
4599
 
4600
4600
  };
@@ -8,6 +8,7 @@ import 'dayjs/locale/en.js';
8
8
  import { aggregateEyeTestConfig, findOneEyeTestConfig, updateOneEyeTestConfig } from '../service/eyeTestConfiguration.service.js';
9
9
  import { ObjectId } from 'mongodb';
10
10
  import { aggregateClusters } from '../service/clusters.service.js';
11
+ import { sendComplianceEmail } from '../validation/eyeTest.validation.js';
11
12
 
12
13
  dayjs.extend( utc );
13
14
  dayjs.extend( timezone );
@@ -750,6 +751,7 @@ export async function save( req, res ) {
750
751
  try {
751
752
  const inputData = req.body;
752
753
  const openSearch = JSON.parse( process.env.OPENSEARCH );
754
+ const ses = JSON.parse( process.env.SES );
753
755
 
754
756
  let searchFilter =[
755
757
  {
@@ -811,7 +813,6 @@ export async function save( req, res ) {
811
813
  },
812
814
  };
813
815
  const getOutput = await getOpenSearchData( openSearch.EyeTestOutput, searchQuery );
814
- logger.info( { getOutput: getOutput } );
815
816
  const output = getOutput?.body?.hits?.hits?.length > 0 ? getOutput?.body?.hits?.hits[0] : null;
816
817
  if ( !output ) {
817
818
  return res.sendError( 'This file is currently being audited by another user. Any actions you perform will not be saved.', 400 );
@@ -845,19 +846,46 @@ export async function save( req, res ) {
845
846
 
846
847
  const getEyetestConfig = await findOneEyeTestConfig( { clientId: req?.user.userType == 'tango'? inputData?.clientId : req.user.clientId, configureType: 'email' }, { complianceThreshold: 1 } );
847
848
 
848
- if ( getEyetestConfig && inputData.auditStatus == 'Audited' && inputUpdatedData ) {
849
+ if ( getEyetestConfig &&( inputData.auditStatus == 'Audited' ||inputData.auditStatus == 'Re-Audited' ) && inputUpdatedData && inputData.type === 'physical' && inputData?.isReSend !== false ) {
849
850
  const trustScore = Math.round( 100-( ( inputData?.overRides / inputData?.steps?.length ) * 100 ) );
850
851
  const value = Number( getEyetestConfig?.complianceThreshold?.value?.split( '%' )[0] );
851
852
  const condition = getEyetestConfig?.complianceThreshold?.condition;
852
- const id= `${inputData?.RMEmail}_${inputData?.cluster}_${inputUpdatedData?._source?.optumId}_${inputData?.fileId}`;
853
+ const id= inputData?.isReSend === true ?`${inputData.id}_${inputUpdatedData?._source?.optumId}_${inputData?.fileId}_${Date.now()}` : `${inputData.id}_${inputUpdatedData?._source?.optumId}_${inputData?.fileId}`;
854
+ const humanScore = Math.round( ( inputUpdatedData?._source?.auditedSteps /inputUpdatedData?._source?.totalSteps ) * 100 );
855
+ const userName = await findOneUser( { _id: new mongoose.Types.ObjectId( inputUpdatedData?._source?.userId ) }, { userName: 1 } );
856
+ const order = [
857
+ 'Distance-VA-Check',
858
+ 'Torchlight Check',
859
+ 'JCC Check',
860
+ 'Duochrome Check',
861
+ 'Near Addition Check',
862
+ 'Prescription Verification Check',
863
+ ];
864
+ const steps = inputData?.steps
865
+ .filter( ( step ) =>
866
+ step.hasOwnProperty( 'auditedStatus' ) ?
867
+ step.auditedStatus === false :
868
+ step.AIStatus === false,
869
+ )
870
+ .map( ( step ) => ( {
871
+ stepName: step.processName,
872
+ comment: step?.reason === 'others'? step.otherReason:step?.reason,
873
+ } ) );
874
+ steps.sort(
875
+ ( a, b ) => order.indexOf( a.processName ) - order.indexOf( b.processName ),
876
+ );
853
877
  switch ( condition ) {
854
878
  case '>': if ( trustScore > value ) {
855
879
  const record1 = {
856
880
  RMEmail: inputData.RMEmail,
881
+ RMName: inputData.RMEmail,
857
882
  cluster: inputData.cluster,
858
883
  optumId: inputUpdatedData?._source?.optumId,
884
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
859
885
  storeName: inputUpdatedData?._source?.storeName,
860
886
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
887
+ humanScore: humanScore,
888
+ auditedBy: userName?.userName || '',
861
889
  trustScore: inputUpdatedData?._source?.trustScore,
862
890
  auditedId: inputUpdatedData?._id,
863
891
  queueId: inputUpdatedData?._source?.queueId,
@@ -866,15 +894,26 @@ export async function save( req, res ) {
866
894
 
867
895
  };
868
896
  await insertWithId( openSearch.eyeTestEmailAlert, id, record1 );
897
+ sendComplianceEmail( {
898
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
899
+ fromEmail: ses.eyeTestAlertEmail,
900
+ userName: 'User',
901
+ compliancePercentage: humanScore,
902
+ steps: steps,
903
+ } );
869
904
  }
870
905
  break;
871
906
  case '<': if ( trustScore < value ) {
872
907
  const record2 = {
873
908
  RMEmail: inputData.RMEmail,
909
+ RMName: inputData.RMEmail,
874
910
  cluster: inputData.cluster,
875
911
  optumId: inputUpdatedData?._source?.optumId,
912
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
876
913
  storeName: inputUpdatedData?._source?.storeName,
877
914
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
915
+ humanScore: humanScore,
916
+ auditedBy: userName?.userName || '',
878
917
  trustScore: inputUpdatedData?._source?.trustScore,
879
918
  auditedId: inputUpdatedData?._id,
880
919
  queueId: inputUpdatedData?._source?.queueId,
@@ -883,15 +922,26 @@ export async function save( req, res ) {
883
922
 
884
923
  };
885
924
  await insertWithId( openSearch.eyeTestEmailAlert, id, record2 );
925
+ sendComplianceEmail( {
926
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
927
+ fromEmail: ses.eyeTestAlertEmail,
928
+ userName: 'User',
929
+ compliancePercentage: humanScore,
930
+ steps: steps,
931
+ } );
886
932
  };
887
933
  break;
888
934
  case '>=': if ( trustScore >= value ) {
889
935
  const record3 = {
890
936
  RMEmail: inputData.RMEmail,
937
+ RMName: inputData.RMEmail,
891
938
  cluster: inputData.cluster,
892
939
  optumId: inputUpdatedData?._source?.optumId,
940
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
893
941
  storeName: inputUpdatedData?._source?.storeName,
894
942
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
943
+ humanScore: humanScore,
944
+ auditedBy: userName?.userName || '',
895
945
  trustScore: inputUpdatedData?._source?.trustScore,
896
946
  auditedId: inputUpdatedData?._id,
897
947
  queueId: inputUpdatedData?._source?.queueId,
@@ -900,15 +950,26 @@ export async function save( req, res ) {
900
950
 
901
951
  };
902
952
  await insertWithId( openSearch.eyeTestEmailAlert, id, record3 );
953
+ sendComplianceEmail( {
954
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
955
+ fromEmail: ses.eyeTestAlertEmail,
956
+ userName: 'User',
957
+ compliancePercentage: humanScore,
958
+ steps: steps,
959
+ } );
903
960
  }
904
961
  break;
905
962
  case '<=': if ( trustScore <= value ) {
906
963
  const record4 = {
907
964
  RMEmail: inputData.RMEmail,
965
+ RMName: inputData.RMEmail,
908
966
  cluster: inputData.cluster,
909
967
  optumId: inputUpdatedData?._source?.optumId,
968
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
910
969
  storeName: inputUpdatedData?._source?.storeName,
911
970
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
971
+ humanScore: humanScore,
972
+ auditedBy: userName?.userName || '',
912
973
  trustScore: inputUpdatedData?._source?.trustScore,
913
974
  auditedId: inputUpdatedData?._id,
914
975
  queueId: inputUpdatedData?._source?.queueId,
@@ -917,16 +978,27 @@ export async function save( req, res ) {
917
978
 
918
979
  };
919
980
  await insertWithId( openSearch.eyeTestEmailAlert, id, record4 );
981
+ sendComplianceEmail( {
982
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
983
+ fromEmail: ses.eyeTestAlertEmail,
984
+ userName: 'User',
985
+ compliancePercentage: humanScore,
986
+ steps: steps,
987
+ } );
920
988
  }
921
989
  break;
922
990
  case '=':
923
991
  case '==': if ( trustScore == value ) {
924
992
  const record5 = {
925
993
  RMEmail: inputData.RMEmail,
994
+ RMName: inputData.RMEmail,
926
995
  cluster: inputData.cluster,
927
996
  optumId: inputUpdatedData?._source?.optumId,
997
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
928
998
  storeName: inputUpdatedData?._source?.storeName,
929
999
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1000
+ humanScore: humanScore,
1001
+ auditedBy: userName?.userName || '',
930
1002
  trustScore: inputUpdatedData?._source?.trustScore,
931
1003
  auditedId: inputUpdatedData?._id,
932
1004
  queueId: inputUpdatedData?._source?.queueId,
@@ -935,15 +1007,26 @@ export async function save( req, res ) {
935
1007
 
936
1008
  };
937
1009
  await insertWithId( openSearch.eyeTestEmailAlert, id, record5 );
1010
+ sendComplianceEmail( {
1011
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1012
+ fromEmail: ses.eyeTestAlertEmail,
1013
+ userName: 'User',
1014
+ compliancePercentage: humanScore,
1015
+ steps: steps,
1016
+ } );
938
1017
  }
939
1018
  break;
940
1019
  case '===': if ( trustScore === value ) {
941
1020
  const record6 = {
942
1021
  RMEmail: inputData.RMEmail,
1022
+ RMName: inputData.RMEmail,
943
1023
  cluster: inputData.cluster,
944
1024
  optumId: inputUpdatedData?._source?.optumId,
1025
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
945
1026
  storeName: inputUpdatedData?._source?.storeName,
946
1027
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1028
+ humanScore: humanScore,
1029
+ auditedBy: userName?.userName || '',
947
1030
  trustScore: inputUpdatedData?._source?.trustScore,
948
1031
  auditedId: inputUpdatedData?._id,
949
1032
  queueId: inputUpdatedData?._source?.queueId,
@@ -951,12 +1034,19 @@ export async function save( req, res ) {
951
1034
  updatedAt: new Date(),
952
1035
  };
953
1036
  await insertWithId( openSearch.eyeTestEmailAlert, id, record6 );
1037
+ sendComplianceEmail( {
1038
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1039
+ fromEmail: ses.eyeTestAlertEmail,
1040
+ userName: 'User',
1041
+ compliancePercentage: humanScore,
1042
+ steps: steps,
1043
+ } );
954
1044
  }
955
1045
  break;
956
1046
  default: throw new Error( `Unknown condition: ${condition}` );
957
1047
  }
958
1048
  }
959
- return res.sendSuccess( 'The file has been submited successfully' );
1049
+ return res.sendSuccess( 'The file has been submitted successfully' );
960
1050
  } catch ( error ) {
961
1051
  const err = error.message || 'Internal Server Error';
962
1052
  logger.error( { message: error, data: req.params, function: 'eyetestAudit-controller-save' } );
@@ -1198,18 +1288,25 @@ export async function userAuditedData( req, res ) {
1198
1288
 
1199
1289
  export async function summaryList( req, res ) {
1200
1290
  try {
1291
+ // declare inputData and get request body
1201
1292
  const inputData = req.body;
1293
+
1294
+ // throw 204 if client id is not 11
1202
1295
  if ( inputData.clientId !== '11' ) {
1203
1296
  return res.sendError( 'No data found', 204 );
1204
1297
  }
1205
1298
 
1299
+ // declare openSearch and get data form the .env/secret manager
1206
1300
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1301
+
1302
+ // set limit, offset, sortBy, sortOrder value if not set default value
1207
1303
  const limit = inputData.limit || 10;
1208
1304
  const offset = inputData.offset ? ( inputData.offset - 1 ) * limit : 0;
1209
1305
 
1210
1306
  const sortBy = inputData?.sortBy ? [ 'coveredStepsAI', 'trustScore', 'date', 'ComplianceScore', 'auditedSteps' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'storeName.keyword';
1211
1307
  const order = inputData?.sortOrder || -1;
1212
1308
 
1309
+ // declare filter and assign range values from inputData
1213
1310
  let filter= [
1214
1311
  {
1215
1312
  'range': {
@@ -1222,6 +1319,7 @@ export async function summaryList( req, res ) {
1222
1319
 
1223
1320
  ];
1224
1321
 
1322
+ // filter by Audit Status if key has minimum one element
1225
1323
  if ( inputData?.auditStatus && inputData?.auditStatus?.length > 0 ) {
1226
1324
  filter.push( {
1227
1325
  'terms': {
@@ -1229,6 +1327,8 @@ export async function summaryList( req, res ) {
1229
1327
  },
1230
1328
  } );
1231
1329
  }
1330
+
1331
+ // filter by Visitors Type if key has minimum one element
1232
1332
  if ( inputData?.demographics && inputData?.demographics?.length > 0 ) {
1233
1333
  filter.push( {
1234
1334
  'terms': {
@@ -1236,6 +1336,8 @@ export async function summaryList( req, res ) {
1236
1336
  },
1237
1337
  } );
1238
1338
  }
1339
+
1340
+ // filter by Eye test Type if key has string value one element
1239
1341
  if ( inputData.type && inputData.type!='' ) {
1240
1342
  filter.push( {
1241
1343
  'term': {
@@ -1245,6 +1347,17 @@ export async function summaryList( req, res ) {
1245
1347
  } );
1246
1348
  }
1247
1349
 
1350
+ // filter by near addition if value != "" and eye test must be a physical
1351
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
1352
+ filter.push( {
1353
+ 'term': {
1354
+ 'nearAddition.keyword': inputData.nearAddition,
1355
+ },
1356
+
1357
+ } );
1358
+ }
1359
+
1360
+ // filter by stores if key has minimum one element else throw an error
1248
1361
  if ( inputData.storeId && inputData.storeId.length > 0 ) {
1249
1362
  filter.push( {
1250
1363
 
@@ -1257,11 +1370,14 @@ export async function summaryList( req, res ) {
1257
1370
  return res.sendError( 'No data found', 204 );
1258
1371
  }
1259
1372
 
1373
+ // Declare mustQuery as an array and include all elements from the filter array
1260
1374
  let mustQuery = [
1261
1375
  ...filter,
1262
1376
  ];
1263
1377
 
1264
- if ( inputData.complianceScore && inputData.complianceScore?.length > null ) {
1378
+ // filter by complianceScore/tango score if key has minimum one element
1379
+ if ( inputData.complianceScore && inputData.complianceScore?.length > 0 ) {
1380
+ // If the key type is not 'store', split the compliance score ranges
1265
1381
  if ( inputData.keyType !== 'store' ) {
1266
1382
  const rangeShould = inputData?.complianceScore?.map( ( r ) => {
1267
1383
  const [ gte, lte ] = r.split( '-' ).map( Number );
@@ -1271,7 +1387,7 @@ export async function summaryList( req, res ) {
1271
1387
  },
1272
1388
  };
1273
1389
  } );
1274
-
1390
+ // Add the range conditions to mustQuery (any one of the ranges should match)
1275
1391
  mustQuery.push(
1276
1392
  {
1277
1393
  bool: {
@@ -1280,7 +1396,7 @@ export async function summaryList( req, res ) {
1280
1396
  },
1281
1397
  },
1282
1398
  );
1283
- } else {
1399
+ } else { // If-else condition: use 'terms' filter to match records by Compliance Score / Tango Score
1284
1400
  const values = Array.isArray( inputData.complianceScore ) ?
1285
1401
  inputData.complianceScore.map( Number ) :
1286
1402
  [ Number( inputData.complianceScore ) ];
@@ -1293,6 +1409,7 @@ export async function summaryList( req, res ) {
1293
1409
  }
1294
1410
  }
1295
1411
 
1412
+ // filter by category if key exist allow "fake, genuine" else throw valid category
1296
1413
  if ( inputData?.category ) {
1297
1414
  switch ( inputData?.category ) {
1298
1415
  case 'fake':
@@ -2118,6 +2235,15 @@ export async function summaryCard( req, res ) {
2118
2235
  },
2119
2236
  } );
2120
2237
  }
2238
+
2239
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
2240
+ filter.push( {
2241
+ 'term': {
2242
+ 'nearAddition.keyword': inputData.nearAddition,
2243
+ },
2244
+
2245
+ } );
2246
+ }
2121
2247
  let mustQuery = [
2122
2248
  ...filter,
2123
2249
  ];
@@ -2271,7 +2397,7 @@ export async function summaryCard( req, res ) {
2271
2397
  'humanScore': {
2272
2398
  filter: {
2273
2399
  terms: {
2274
- 'auditStatus.keyword': [ 'Audited', 'ReAudited' ],
2400
+ 'auditStatus.keyword': [ 'Audited', 'Re-Audited' ],
2275
2401
  },
2276
2402
  },
2277
2403
  aggs: {
@@ -2386,7 +2512,7 @@ export async function emailAlertLog( req, res ) {
2386
2512
  inputData.clientId = req.user.userType == 'tango'? inputData?.clientId : req?.user?.clientId;
2387
2513
  // get openserach name form the env file
2388
2514
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2389
-
2515
+ const sortBy = inputData?.sortBy ? [ 'complianceScore', 'humanScore', 'trustScore', 'updatedAt', 'createdAt' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'RMName.keyword';
2390
2516
 
2391
2517
  let filter =[
2392
2518
  {
@@ -2399,7 +2525,7 @@ export async function emailAlertLog( req, res ) {
2399
2525
  },
2400
2526
  ];
2401
2527
  let sort =[
2402
- { [inputData?.sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2528
+ { [sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2403
2529
  ];
2404
2530
  if ( inputData.RmList && inputData.RmList?.lenght > 0 ) {
2405
2531
  filter.push( {
@@ -2416,19 +2542,86 @@ export async function emailAlertLog( req, res ) {
2416
2542
  },
2417
2543
  } );
2418
2544
  }
2545
+ let search = {
2546
+ must: filter,
2547
+ };
2548
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
2549
+ search ={
2550
+ 'must': filter,
2551
+ 'should': [
2552
+ {
2553
+ 'wildcard': {
2554
+ 'RMEmail.keyword': {
2555
+ 'value': `*${inputData.searchValue}*`,
2556
+ },
2557
+ },
2558
+ },
2559
+ {
2560
+ 'wildcard': {
2561
+ 'RMName.keyword': {
2562
+ 'value': `*${inputData.searchValue}*`,
2563
+ },
2564
+ },
2565
+ },
2566
+ {
2567
+ 'wildcard': {
2568
+ 'cluster.keyword': {
2569
+ 'value': `*${inputData.searchValue}*`,
2570
+ },
2571
+ },
2572
+ },
2573
+ {
2574
+ 'wildcard': {
2575
+ 'optumId.keyword': {
2576
+ 'value': `*${inputData.searchValue}*`,
2577
+ },
2578
+ },
2579
+ },
2580
+ {
2581
+ 'wildcard': {
2582
+ 'queueId.keyword': {
2583
+ 'value': `*${inputData.searchValue}*`,
2584
+ },
2585
+ },
2586
+ },
2587
+ {
2588
+ 'wildcard': {
2589
+ 'storeName.keyword': {
2590
+ 'value': `*${inputData.searchValue}*`,
2591
+ },
2592
+ },
2593
+ },
2594
+ {
2595
+ 'wildcard': {
2596
+ 'auditedBy.keyword': {
2597
+ 'value': `*${inputData.searchValue}*`,
2598
+ },
2599
+ },
2600
+ },
2601
+ {
2602
+ 'wildcard': {
2603
+ 'optomEmailId.keyword': {
2604
+ 'value': `*${inputData.searchValue}*`,
2605
+ },
2606
+ },
2607
+ },
2608
+ ],
2609
+ 'minimum_should_match': 1,
2610
+ };
2611
+ }
2419
2612
  const getQuery = {
2420
2613
  'size': inputData.isExport == true? 50000 : inputData?.limit || 10,
2421
2614
  'from': offset || 0,
2422
2615
  'query': {
2423
- 'bool': {
2424
- 'must': filter,
2425
- },
2616
+ 'bool': search,
2426
2617
  },
2427
2618
  'sort': sort,
2428
2619
  };
2429
2620
 
2621
+
2430
2622
  // get query result from the open saerch
2431
2623
  const getData= await getOpenSearchData( openSearch.eyeTestEmailAlert, getQuery );
2624
+ const count = getData?.body?.hits?.total?.value;
2432
2625
  // throw error if got null or empty array
2433
2626
  if ( !getData || getData?.body?.hits?.hits?.length == 0 ) {
2434
2627
  return res.sendError( 'No records found', 204 );
@@ -2436,30 +2629,40 @@ export async function emailAlertLog( req, res ) {
2436
2629
  let response =[];
2437
2630
  for ( let item of getData?.body?.hits?.hits ) {
2438
2631
  response.push( {
2439
- RMEmail: item?._source?.RMEmail,
2632
+ RMEmail: item?._source?.RMEmail || '',
2633
+ RMName: item?._source?.RMName || '',
2440
2634
  cluster: item?._source?.cluster,
2441
2635
  optumId: item?._source?.optumId,
2636
+ optomEmailId: item?._source?.optomEmailId,
2442
2637
  storeName: item?._source?.storeName,
2443
- complianceScore: item?._source?.complianceScore,
2444
- trustScore: item?._source?.trustScore,
2638
+ complianceScore: Math.round( item?._source?.complianceScore || 0 ),
2639
+ humanScore: Math.round( item?._source?.humanScore || 0 ),
2640
+ trustScore: item?._source?.trustScore || '--',
2445
2641
  auditedId: item?._source?.auditedId,
2446
2642
  queueId: item?._source?.queueId,
2643
+ auditedBy: item?._source?.auditedBy || '--',
2447
2644
 
2448
2645
  } );
2449
2646
  }
2450
2647
 
2451
2648
  if ( inputData.isExport == true ) {
2649
+ logger.info( { response: response } );
2452
2650
  const chunkedMappingData = await chunkArray( response, 10 );
2453
2651
  const promises = chunkedMappingData.map( async ( chunk ) => {
2454
2652
  const exportData = [];
2455
2653
  for ( const element of chunk ) {
2456
2654
  exportData.push( {
2457
- 'RM Email': element.RMEmail,
2655
+ 'RM Name': element.RMEmail,
2458
2656
  'Cluster Name': element.cluster,
2459
- 'Optom Id': element.optumId,
2657
+ 'Optom ID': element.optumId,
2658
+ 'Optom Email': element.optomEmailId || '--',
2460
2659
  'Store Name': element.storeName,
2461
- 'Tango Score': element.complianceScore || 0,
2462
- 'Trust Score': element.trustScore || 0,
2660
+ 'Audited By': element.auditedBy || '--',
2661
+ 'Tango Score': Math.round( element.complianceScore || 0 ),
2662
+
2663
+ 'Trust Score': element.trustScore || '--',
2664
+ 'Human Audited Score': Math.round( element.humanScore || 0 ),
2665
+
2463
2666
 
2464
2667
  } );
2465
2668
  }
@@ -2468,6 +2671,7 @@ export async function emailAlertLog( req, res ) {
2468
2671
  } );
2469
2672
  const mappedArrays = await Promise.all( promises );
2470
2673
  const result = mappedArrays.flat();
2674
+ logger.info( { result: result } );
2471
2675
  await download( result, res );
2472
2676
  return;
2473
2677
  }
@@ -2477,7 +2681,7 @@ export async function emailAlertLog( req, res ) {
2477
2681
 
2478
2682
 
2479
2683
  logger.info( { inputData: inputData, getData: response } );
2480
- return res.sendSuccess( { result: response, count: response.length } );
2684
+ return res.sendSuccess( { result: response, count: count } );
2481
2685
  } catch ( error ) {
2482
2686
  const err = error.message || 'Internal Server Error';
2483
2687
  logger.error( { message: error, data: req.body, function: 'eyetestAudit-controller-emailAlertLog' } );
@@ -2637,10 +2841,6 @@ export async function getEmailConfig( req, res ) {
2637
2841
  configureType: 'email',
2638
2842
  };
2639
2843
  const getOne = await findOneEyeTestConfig( query, { complianceThreshold: 1, _id: 1 } );
2640
- logger.info( { getOne: getOne, message: 'getEmailConfig' } );
2641
- // if ( !getOne?.complianceThreshold ) {
2642
- // return res.sendError( 'No data found', 204 );
2643
- // }
2644
2844
  if ( !getOne ) {
2645
2845
  return res.sendError( ' No data found', 204 );
2646
2846
  }
@@ -64,6 +64,7 @@ export const saveSchema = joi.object( {
64
64
  spokenLanguage: joi.string().optional(),
65
65
  cluster: joi.string().required().allow( '' ),
66
66
  RMEmail: joi.string().required().allow( '' ),
67
+ isReSend: joi.boolean().optional(),
67
68
  } );
68
69
 
69
70
  export const saveValid = {
@@ -156,7 +157,7 @@ export const summaryListSchema = joi.object( {
156
157
  ).optional(),
157
158
  storeId: joi.array().items( joi.string().required() ).required(),
158
159
  complianceScore: joi.array().items( joi.string().required() ).optional(),
159
- nearAddition: joi.string().optional().allow( '' ),
160
+ nearAddition: joi.string().optional().allow( '', 'true', 'false' ),
160
161
  isExport: joi.boolean().optional(),
161
162
  searchValue: joi.string().optional().allow( '' ),
162
163
  sortBy: joi.string().optional(),
@@ -234,7 +235,7 @@ export const summaryCardSchema = joi.object( {
234
235
  ).optional(),
235
236
  storeId: joi.array().items( joi.string().required() ).required(),
236
237
  complianceScore: joi.array().items( joi.string().required() ).optional(),
237
- nearAddition: joi.string().optional().allow( '' ),
238
+ nearAddition: joi.string().optional().allow( '', 'true', 'false' ),
238
239
  category: joi.string().optional() .valid( 'fake', 'genuine' ),
239
240
  } );
240
241
 
@@ -279,12 +280,8 @@ export const emailAlertLogSchema = joi.object( {
279
280
 
280
281
  return value;
281
282
  } ),
282
- type: joi.string().required().messages( {
283
- 'any.required': 'Type is required',
284
- 'string.empty': 'Type cannot be empty',
285
- } ),
286
283
  RMList: joi.array().items(
287
- joi.string().optional(),
284
+ joi.string().required(),
288
285
  ).optional(),
289
286
  clientId: joi.string().when( '$userType', {
290
287
  is: 'tango',
@@ -294,7 +291,7 @@ export const emailAlertLogSchema = joi.object( {
294
291
  } ),
295
292
  otherwise: joi.optional(),
296
293
  } ),
297
- cluster: joi.array().items( joi.string().required() ).required(),
294
+ cluster: joi.array().items( joi.string().required() ).optional(),
298
295
  isExport: joi.boolean().optional(),
299
296
  searchValue: joi.string().optional().allow( '' ),
300
297
  sortBy: joi.string().optional(),
@@ -413,6 +410,7 @@ export const getEmailConfigSchema = joi.object( {
413
410
  } ),
414
411
  otherwise: joi.optional(),
415
412
  } ),
413
+
416
414
  } );
417
415
 
418
416
  export const getEmailConfigValid = {
@@ -3,14 +3,14 @@ import { isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
3
3
  import { accessVerification } from 'tango-app-api-middleware';
4
4
  import { cancelValid, clusterListValid, emailAlertLogValid, getEmailConfigValid, getFileHistoryValid, getFileValid, getListValid, getUserConfigValid, rmListValid, saveValid, summaryCardValid, summaryListValid, updateEmailConfigValid, updateUserConfigValid, userAuditedDataValid, viewFileValid } from '../dtos/eyeTestAudit.dtos.js';
5
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';
6
- import { isSuperAdmin, roleValidation } from '../validation/eyeTest.validation.js';
6
+ import { isSuperAdmin, roleValidation, sendAlert } from '../validation/eyeTest.validation.js';
7
7
 
8
8
  export const eyeTestAuditRouter = Router();
9
9
 
10
10
  eyeTestAuditRouter.post( '/get-list', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getListValid ), getList );
11
11
  eyeTestAuditRouter.get( '/view-file/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( viewFileValid ), viewFile );
12
12
  eyeTestAuditRouter.get( '/get-file/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getFileValid ), getFile );
13
- eyeTestAuditRouter.post( '/save', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( saveValid ), save );
13
+ eyeTestAuditRouter.post( '/save', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( saveValid ), sendAlert, save );
14
14
  eyeTestAuditRouter.post( '/cancel', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( cancelValid ), cancel );
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 );
@@ -1,4 +1,5 @@
1
- import { logger } from 'tango-app-api-middleware';
1
+ import { getOpenSearchById, logger } from 'tango-app-api-middleware';
2
+ import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
2
3
 
3
4
  export async function roleValidation( req, res, next ) {
4
5
  try {
@@ -39,3 +40,133 @@ export async function isSuperAdmin( req, res, next ) {
39
40
  }
40
41
  }
41
42
 
43
+ export async function sendAlert( req, res, next ) {
44
+ try {
45
+ const inputData = req.body;
46
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
47
+ if ( inputData.auditStatus == 'Re-Audited' && inputData.type == 'physical' && inputData?.isReSend == undefined ) {
48
+ const getOne = await getOpenSearchById( openSearch.EyeTestInput, inputData.id );
49
+ const id = `${inputData.id}_${getOne?.body?._source?.optumId}_${inputData.fileId}`;
50
+ const getId = await getOpenSearchById( openSearch.eyeTestEmailAlert, id );
51
+ logger.info( { getId: getId, id: id } );
52
+ const inputUpdatedData = getId?.body;
53
+ if ( inputUpdatedData ) {
54
+ return res.sendSuccess( 'Do you want resend mail?', 202 );
55
+ } else {
56
+ return next();
57
+ }
58
+ } else {
59
+ return next();
60
+ }
61
+ } catch ( error ) {
62
+ const err = error.message;
63
+ return res.sendError( err, 500 );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Send HTML Email using AWS SES
69
+ * @param {Object} params
70
+ * @param {string} params.toEmail - Recipient email address
71
+ * @param {string} params.userName - Recipient name
72
+ * @param {number} params.compliancePercentage - Compliance score
73
+ */
74
+ export async function sendComplianceEmail( {
75
+ toEmail,
76
+ fromEmail, // sender email added here
77
+ userName,
78
+ compliancePercentage,
79
+ steps,
80
+ } ) {
81
+ // Initialize AWS SES client
82
+ logger.info( { steps: steps } );
83
+ const sesClient = new SESClient( {
84
+ region: 'ap-south-1', // Change to your AWS region
85
+ credentials: {
86
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID_SIGNED_URL,
87
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY__SIGNED_URL,
88
+ },
89
+ } );
90
+ const htmlBody = ( userName, steps, compliancePercentage ) => {
91
+ const tableRows = steps
92
+ .map(
93
+ ( item ) => `
94
+ <tr>
95
+ <td>${item.stepName}</td>
96
+ <td>${item.comment?? '--'}</td>
97
+ </tr>
98
+ `,
99
+ )
100
+ .join( '' );
101
+
102
+ return `
103
+ <!DOCTYPE html>
104
+ <html>
105
+ <head>
106
+ <meta charset="UTF-8" />
107
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
108
+ <title>Non Compliance Report</title>
109
+ <style>
110
+ body { font-family: 'Inter', sans-serif; margin: 0; padding: 24px; background: #f9fafb; color: #121a26; }
111
+ .container { max-width: 700px; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
112
+ .title { font-size: 24px; font-weight: 700; color: #121a26; }
113
+ .subtitle { font-size: 16px; color: #384860; line-height: 1.5; margin-top: 8px; }
114
+ .table { width: 100%; border-collapse: collapse; margin-top: 16px; }
115
+ .table th, .table td { text-align: left; padding: 12px 16px; border-bottom: 1px solid #eaeef0; color: #667085; }
116
+ .footer { margin-top: 32px; font-size: 14px; color: #384860; }
117
+ .compliance { font-size: 18px; font-weight: 700; color: #f04438; margin-top: 16px; }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div class="container">
122
+ <p class="title">Non Compliance on Eye Test Procedures</p>
123
+ <p class="subtitle">
124
+ Dear ${userName},<br><br>
125
+ Please ensure that all conducted eye tests meet the required compliance standards.<br>
126
+ Your prompt attention to this matter will help us maintain the highest standards and avoid any discrepancies.<br>
127
+ Please take a look at the below findings.
128
+ </p>
129
+
130
+ <table class="table">
131
+ <thead>
132
+ <tr>
133
+ <th>Step Name</th>
134
+ <th>Comments</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ ${tableRows}
139
+ </tbody>
140
+ </table>
141
+
142
+ <p class="compliance">Compliance Percentage: ${compliancePercentage}%</p>
143
+ <p class="footer">Regards,<br>Team Tango</p>
144
+ </div>
145
+ </body>
146
+ </html>`;
147
+ };
148
+
149
+ const emailHTML = htmlBody( userName, steps, compliancePercentage );
150
+
151
+ const params = {
152
+ Source: fromEmail, // sender email (must be verified in SES)
153
+ Destination: {
154
+ ToAddresses: [ toEmail ],
155
+ },
156
+ Message: {
157
+ Subject: { Data: 'Non Compliance on Eye Test Procedures' },
158
+ Body: { Html: { Data: emailHTML } },
159
+ },
160
+ };
161
+
162
+ try {
163
+ const response = await sesClient.send( new SendEmailCommand( params ) );
164
+ // console.log( 'Email sent successfully:', response.MessageId );
165
+ return response;
166
+ } catch ( error ) {
167
+ // console.error( 'Error sending email:', error );
168
+ throw error;
169
+ }
170
+ }
171
+
172
+