tango-app-api-audit 3.6.27 → 3.6.30

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.27",
3
+ "version": "3.6.30",
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,8 @@ 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 );
755
+ const url = JSON.parse( process.env.URL );
753
756
 
754
757
  let searchFilter =[
755
758
  {
@@ -811,7 +814,6 @@ export async function save( req, res ) {
811
814
  },
812
815
  };
813
816
  const getOutput = await getOpenSearchData( openSearch.EyeTestOutput, searchQuery );
814
- logger.info( { getOutput: getOutput } );
815
817
  const output = getOutput?.body?.hits?.hits?.length > 0 ? getOutput?.body?.hits?.hits[0] : null;
816
818
  if ( !output ) {
817
819
  return res.sendError( 'This file is currently being audited by another user. Any actions you perform will not be saved.', 400 );
@@ -845,19 +847,76 @@ export async function save( req, res ) {
845
847
 
846
848
  const getEyetestConfig = await findOneEyeTestConfig( { clientId: req?.user.userType == 'tango'? inputData?.clientId : req.user.clientId, configureType: 'email' }, { complianceThreshold: 1 } );
847
849
 
848
- if ( getEyetestConfig && inputData.auditStatus == 'Audited' && inputUpdatedData ) {
849
- const trustScore = Math.round( 100-( ( inputData?.overRides / inputData?.steps?.length ) * 100 ) );
850
+ if ( getEyetestConfig &&( inputData.auditStatus == 'Audited' ||inputData.auditStatus == 'Re-Audited' ) && inputUpdatedData && inputData.type === 'physical' && inputData?.isReSend !== false ) {
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 emailList = inputData?.RMEmail?.split( ',' );
857
+ let rmName;
858
+
859
+ if ( emailList?.length > 0 ) {
860
+ const userQuery = [
861
+ {
862
+ $match: {
863
+ $and: [
864
+ {
865
+ email: { $in: emailList },
866
+ },
867
+ ],
868
+ },
869
+ },
870
+ {
871
+ $group: {
872
+ _id: null,
873
+ userList: { $addToSet: '$userName' },
874
+ },
875
+ },
876
+ {
877
+ $project: {
878
+ userList: 1,
879
+ _id: 0,
880
+ },
881
+ },
882
+ ];
883
+ const getRMName = await aggregateUser( userQuery );
884
+ rmName = getRMName?.[0]?.userList || '';
885
+ }
886
+
887
+ const order = [
888
+ 'Distance-VA-Check',
889
+ 'Torchlight Check',
890
+ 'JCC Check',
891
+ 'Duochrome Check',
892
+ 'Near Addition Check',
893
+ 'Prescription Verification Check',
894
+ ];
895
+ const steps = inputData?.steps
896
+ .filter( ( step ) =>
897
+ step.hasOwnProperty( 'auditedStatus' ) ?
898
+ step.auditedStatus === false :
899
+ step.AIStatus === false,
900
+ )
901
+ .map( ( step ) => ( {
902
+ stepName: step.processName,
903
+ comment: step?.reason === 'others'? step.otherReason:step?.reason,
904
+ } ) );
905
+ steps.sort(
906
+ ( a, b ) => order.indexOf( a.processName ) - order.indexOf( b.processName ),
907
+ );
853
908
  switch ( condition ) {
854
- case '>': if ( trustScore > value ) {
909
+ case '>': if ( humanScore > value ) {
855
910
  const record1 = {
856
911
  RMEmail: inputData.RMEmail,
912
+ RMName: rmName.toString(),
857
913
  cluster: inputData.cluster,
858
914
  optumId: inputUpdatedData?._source?.optumId,
915
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
859
916
  storeName: inputUpdatedData?._source?.storeName,
860
917
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
918
+ humanScore: humanScore,
919
+ auditedBy: userName?.userName || '',
861
920
  trustScore: inputUpdatedData?._source?.trustScore,
862
921
  auditedId: inputUpdatedData?._id,
863
922
  queueId: inputUpdatedData?._source?.queueId,
@@ -866,15 +925,29 @@ export async function save( req, res ) {
866
925
 
867
926
  };
868
927
  await insertWithId( openSearch.eyeTestEmailAlert, id, record1 );
928
+ sendComplianceEmail( {
929
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
930
+ fromEmail: ses.eyeTestAlertEmail,
931
+ userName: 'User',
932
+ storeName: inputUpdatedData?._source?.storeName,
933
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
934
+ compliancePercentage: humanScore,
935
+ steps: steps,
936
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
937
+ } );
869
938
  }
870
939
  break;
871
- case '<': if ( trustScore < value ) {
940
+ case '<': if ( humanScore < value ) {
872
941
  const record2 = {
873
942
  RMEmail: inputData.RMEmail,
943
+ RMName: rmName.toString(),
874
944
  cluster: inputData.cluster,
875
945
  optumId: inputUpdatedData?._source?.optumId,
946
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
876
947
  storeName: inputUpdatedData?._source?.storeName,
877
948
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
949
+ humanScore: humanScore,
950
+ auditedBy: userName?.userName || '',
878
951
  trustScore: inputUpdatedData?._source?.trustScore,
879
952
  auditedId: inputUpdatedData?._id,
880
953
  queueId: inputUpdatedData?._source?.queueId,
@@ -883,15 +956,29 @@ export async function save( req, res ) {
883
956
 
884
957
  };
885
958
  await insertWithId( openSearch.eyeTestEmailAlert, id, record2 );
959
+ sendComplianceEmail( {
960
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
961
+ fromEmail: ses.eyeTestAlertEmail,
962
+ userName: 'User',
963
+ storeName: inputUpdatedData?._source?.storeName,
964
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
965
+ compliancePercentage: humanScore,
966
+ steps: steps,
967
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
968
+ } );
886
969
  };
887
970
  break;
888
- case '>=': if ( trustScore >= value ) {
971
+ case '>=': if ( humanScore >= value ) {
889
972
  const record3 = {
890
973
  RMEmail: inputData.RMEmail,
974
+ RMName: rmName.toString(),
891
975
  cluster: inputData.cluster,
892
976
  optumId: inputUpdatedData?._source?.optumId,
977
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
893
978
  storeName: inputUpdatedData?._source?.storeName,
894
979
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
980
+ humanScore: humanScore,
981
+ auditedBy: userName?.userName || '',
895
982
  trustScore: inputUpdatedData?._source?.trustScore,
896
983
  auditedId: inputUpdatedData?._id,
897
984
  queueId: inputUpdatedData?._source?.queueId,
@@ -900,15 +987,29 @@ export async function save( req, res ) {
900
987
 
901
988
  };
902
989
  await insertWithId( openSearch.eyeTestEmailAlert, id, record3 );
990
+ sendComplianceEmail( {
991
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
992
+ fromEmail: ses.eyeTestAlertEmail,
993
+ userName: 'User',
994
+ storeName: inputUpdatedData?._source?.storeName,
995
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
996
+ compliancePercentage: humanScore,
997
+ steps: steps,
998
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
999
+ } );
903
1000
  }
904
1001
  break;
905
- case '<=': if ( trustScore <= value ) {
1002
+ case '<=': if ( humanScore <= value ) {
906
1003
  const record4 = {
907
1004
  RMEmail: inputData.RMEmail,
1005
+ RMName: rmName.toString(),
908
1006
  cluster: inputData.cluster,
909
1007
  optumId: inputUpdatedData?._source?.optumId,
1008
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
910
1009
  storeName: inputUpdatedData?._source?.storeName,
911
1010
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1011
+ humanScore: humanScore,
1012
+ auditedBy: userName?.userName || '',
912
1013
  trustScore: inputUpdatedData?._source?.trustScore,
913
1014
  auditedId: inputUpdatedData?._id,
914
1015
  queueId: inputUpdatedData?._source?.queueId,
@@ -917,16 +1018,30 @@ export async function save( req, res ) {
917
1018
 
918
1019
  };
919
1020
  await insertWithId( openSearch.eyeTestEmailAlert, id, record4 );
1021
+ sendComplianceEmail( {
1022
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1023
+ fromEmail: ses.eyeTestAlertEmail,
1024
+ userName: 'User',
1025
+ storeName: inputUpdatedData?._source?.storeName,
1026
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
1027
+ compliancePercentage: humanScore,
1028
+ steps: steps,
1029
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
1030
+ } );
920
1031
  }
921
1032
  break;
922
1033
  case '=':
923
- case '==': if ( trustScore == value ) {
1034
+ case '==': if ( humanScore == value ) {
924
1035
  const record5 = {
925
1036
  RMEmail: inputData.RMEmail,
1037
+ RMName: rmName.toString(),
926
1038
  cluster: inputData.cluster,
927
1039
  optumId: inputUpdatedData?._source?.optumId,
1040
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
928
1041
  storeName: inputUpdatedData?._source?.storeName,
929
1042
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1043
+ humanScore: humanScore,
1044
+ auditedBy: userName?.userName || '',
930
1045
  trustScore: inputUpdatedData?._source?.trustScore,
931
1046
  auditedId: inputUpdatedData?._id,
932
1047
  queueId: inputUpdatedData?._source?.queueId,
@@ -935,15 +1050,29 @@ export async function save( req, res ) {
935
1050
 
936
1051
  };
937
1052
  await insertWithId( openSearch.eyeTestEmailAlert, id, record5 );
1053
+ sendComplianceEmail( {
1054
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1055
+ fromEmail: ses.eyeTestAlertEmail,
1056
+ userName: 'User',
1057
+ storeName: inputUpdatedData?._source?.storeName,
1058
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
1059
+ compliancePercentage: humanScore,
1060
+ steps: steps,
1061
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
1062
+ } );
938
1063
  }
939
1064
  break;
940
- case '===': if ( trustScore === value ) {
1065
+ case '===': if ( humanScore === value ) {
941
1066
  const record6 = {
942
1067
  RMEmail: inputData.RMEmail,
1068
+ RMName: rmName.toString(),
943
1069
  cluster: inputData.cluster,
944
1070
  optumId: inputUpdatedData?._source?.optumId,
1071
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
945
1072
  storeName: inputUpdatedData?._source?.storeName,
946
1073
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1074
+ humanScore: humanScore,
1075
+ auditedBy: userName?.userName || '',
947
1076
  trustScore: inputUpdatedData?._source?.trustScore,
948
1077
  auditedId: inputUpdatedData?._id,
949
1078
  queueId: inputUpdatedData?._source?.queueId,
@@ -951,12 +1080,22 @@ export async function save( req, res ) {
951
1080
  updatedAt: new Date(),
952
1081
  };
953
1082
  await insertWithId( openSearch.eyeTestEmailAlert, id, record6 );
1083
+ sendComplianceEmail( {
1084
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1085
+ fromEmail: ses.eyeTestAlertEmail,
1086
+ userName: 'User',
1087
+ storeName: inputUpdatedData?._source?.storeName,
1088
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
1089
+ compliancePercentage: humanScore,
1090
+ steps: steps,
1091
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
1092
+ } );
954
1093
  }
955
1094
  break;
956
1095
  default: throw new Error( `Unknown condition: ${condition}` );
957
1096
  }
958
1097
  }
959
- return res.sendSuccess( 'The file has been submited successfully' );
1098
+ return res.sendSuccess( 'The file has been submitted successfully' );
960
1099
  } catch ( error ) {
961
1100
  const err = error.message || 'Internal Server Error';
962
1101
  logger.error( { message: error, data: req.params, function: 'eyetestAudit-controller-save' } );
@@ -1198,18 +1337,25 @@ export async function userAuditedData( req, res ) {
1198
1337
 
1199
1338
  export async function summaryList( req, res ) {
1200
1339
  try {
1340
+ // declare inputData and get request body
1201
1341
  const inputData = req.body;
1342
+
1343
+ // throw 204 if client id is not 11
1202
1344
  if ( inputData.clientId !== '11' ) {
1203
1345
  return res.sendError( 'No data found', 204 );
1204
1346
  }
1205
1347
 
1348
+ // declare openSearch and get data form the .env/secret manager
1206
1349
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1350
+
1351
+ // set limit, offset, sortBy, sortOrder value if not set default value
1207
1352
  const limit = inputData.limit || 10;
1208
1353
  const offset = inputData.offset ? ( inputData.offset - 1 ) * limit : 0;
1209
1354
 
1210
1355
  const sortBy = inputData?.sortBy ? [ 'coveredStepsAI', 'trustScore', 'date', 'ComplianceScore', 'auditedSteps' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'storeName.keyword';
1211
1356
  const order = inputData?.sortOrder || -1;
1212
1357
 
1358
+ // declare filter and assign range values from inputData
1213
1359
  let filter= [
1214
1360
  {
1215
1361
  'range': {
@@ -1222,6 +1368,7 @@ export async function summaryList( req, res ) {
1222
1368
 
1223
1369
  ];
1224
1370
 
1371
+ // filter by Audit Status if key has minimum one element
1225
1372
  if ( inputData?.auditStatus && inputData?.auditStatus?.length > 0 ) {
1226
1373
  filter.push( {
1227
1374
  'terms': {
@@ -1229,6 +1376,8 @@ export async function summaryList( req, res ) {
1229
1376
  },
1230
1377
  } );
1231
1378
  }
1379
+
1380
+ // filter by Visitors Type if key has minimum one element
1232
1381
  if ( inputData?.demographics && inputData?.demographics?.length > 0 ) {
1233
1382
  filter.push( {
1234
1383
  'terms': {
@@ -1236,6 +1385,8 @@ export async function summaryList( req, res ) {
1236
1385
  },
1237
1386
  } );
1238
1387
  }
1388
+
1389
+ // filter by Eye test Type if key has string value one element
1239
1390
  if ( inputData.type && inputData.type!='' ) {
1240
1391
  filter.push( {
1241
1392
  'term': {
@@ -1245,6 +1396,17 @@ export async function summaryList( req, res ) {
1245
1396
  } );
1246
1397
  }
1247
1398
 
1399
+ // filter by near addition if value != "" and eye test must be a physical
1400
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
1401
+ filter.push( {
1402
+ 'term': {
1403
+ 'nearAddition.keyword': inputData.nearAddition,
1404
+ },
1405
+
1406
+ } );
1407
+ }
1408
+
1409
+ // filter by stores if key has minimum one element else throw an error
1248
1410
  if ( inputData.storeId && inputData.storeId.length > 0 ) {
1249
1411
  filter.push( {
1250
1412
 
@@ -1257,11 +1419,14 @@ export async function summaryList( req, res ) {
1257
1419
  return res.sendError( 'No data found', 204 );
1258
1420
  }
1259
1421
 
1422
+ // Declare mustQuery as an array and include all elements from the filter array
1260
1423
  let mustQuery = [
1261
1424
  ...filter,
1262
1425
  ];
1263
1426
 
1264
- if ( inputData.complianceScore && inputData.complianceScore?.length > null ) {
1427
+ // filter by complianceScore/tango score if key has minimum one element
1428
+ if ( inputData.complianceScore && inputData.complianceScore?.length > 0 ) {
1429
+ // If the key type is not 'store', split the compliance score ranges
1265
1430
  if ( inputData.keyType !== 'store' ) {
1266
1431
  const rangeShould = inputData?.complianceScore?.map( ( r ) => {
1267
1432
  const [ gte, lte ] = r.split( '-' ).map( Number );
@@ -1271,7 +1436,7 @@ export async function summaryList( req, res ) {
1271
1436
  },
1272
1437
  };
1273
1438
  } );
1274
-
1439
+ // Add the range conditions to mustQuery (any one of the ranges should match)
1275
1440
  mustQuery.push(
1276
1441
  {
1277
1442
  bool: {
@@ -1280,7 +1445,7 @@ export async function summaryList( req, res ) {
1280
1445
  },
1281
1446
  },
1282
1447
  );
1283
- } else {
1448
+ } else { // If-else condition: use 'terms' filter to match records by Compliance Score / Tango Score
1284
1449
  const values = Array.isArray( inputData.complianceScore ) ?
1285
1450
  inputData.complianceScore.map( Number ) :
1286
1451
  [ Number( inputData.complianceScore ) ];
@@ -1293,6 +1458,7 @@ export async function summaryList( req, res ) {
1293
1458
  }
1294
1459
  }
1295
1460
 
1461
+ // filter by category if key exist allow "fake, genuine" else throw valid category
1296
1462
  if ( inputData?.category ) {
1297
1463
  switch ( inputData?.category ) {
1298
1464
  case 'fake':
@@ -1898,7 +2064,9 @@ export async function summaryList( req, res ) {
1898
2064
  'Store ID': element?._source?.storeId || 'Un Assigned',
1899
2065
  'Store Name': element?._source?.storeName ||'Un Assigned',
1900
2066
  'Optom ID': element?._source?.optumId,
2067
+ 'Optom Email ID': element?._source?.salesmanEmailId,
1901
2068
  'Queue ID': element?._source?.queueId,
2069
+ 'Power Types': element?._source?.nearAddition === 'true' ? 'With Near Addition' : element?._source?.nearAddition === 'false' ? 'Without Near Addition':'',
1902
2070
  'Tango Score': `${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.coveredStepsAI/ element?._source?.totalSteps )*100 ): 0} %`,
1903
2071
  'Steps Covered By AI': element?._source?.coveredStepsAI || 0,
1904
2072
  'Human Audited Score': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?`${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.auditedSteps/ element?._source?.totalSteps )*100 ): 0} %`:'NA',
@@ -1923,12 +2091,16 @@ export async function summaryList( req, res ) {
1923
2091
  'Store ID': element?._source?.storeId || 'Un Assigned',
1924
2092
  'Store Name': element?._source?.storeName ||'Un Assigned',
1925
2093
  'Engagement ID': element?._source?.engagementId,
2094
+ 'Optom ID': element?._source?.optm_userId,
2095
+ 'Optom Email ID': element?._source?.userEmail,
2096
+ 'Optom Name': element?._source?.displayName,
1926
2097
  'Tango Score': `${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.coveredStepsAI/ element?._source?.totalSteps )*100 ): 0} %`,
1927
- 'Human Audited Score': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?`${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.auditedSteps/ element?._source?.totalSteps )*100 ): 0} %`:'NA',
1928
2098
  'Steps Covered By AI': element?._source?.coveredStepsAI || 0,
1929
- 'Steps Covered By Human': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?element?._source?.auditedSteps :'NA',
1930
2099
  'Trust Score': element?._source?.trustScore == null || element?._source?.trustScore ==undefined ? 'NA' : `${element?._source?.trustScore} %`,
2100
+ 'Steps Covered By Human': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?element?._source?.auditedSteps :'NA',
2101
+ 'Human Audited Score': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?`${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.auditedSteps/ element?._source?.totalSteps )*100 ): 0} %`:'NA',
1931
2102
  'Test Duration': duration || '',
2103
+ 'Comments ': element._source?.subComment || '',
1932
2104
  'Audit Status': element?._source?.auditStatus || '',
1933
2105
  'Audited By': userName?.userName || '',
1934
2106
 
@@ -2118,6 +2290,15 @@ export async function summaryCard( req, res ) {
2118
2290
  },
2119
2291
  } );
2120
2292
  }
2293
+
2294
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
2295
+ filter.push( {
2296
+ 'term': {
2297
+ 'nearAddition.keyword': inputData.nearAddition,
2298
+ },
2299
+
2300
+ } );
2301
+ }
2121
2302
  let mustQuery = [
2122
2303
  ...filter,
2123
2304
  ];
@@ -2271,7 +2452,7 @@ export async function summaryCard( req, res ) {
2271
2452
  'humanScore': {
2272
2453
  filter: {
2273
2454
  terms: {
2274
- 'auditStatus.keyword': [ 'Audited', 'ReAudited' ],
2455
+ 'auditStatus.keyword': [ 'Audited', 'Re-Audited' ],
2275
2456
  },
2276
2457
  },
2277
2458
  aggs: {
@@ -2386,49 +2567,123 @@ export async function emailAlertLog( req, res ) {
2386
2567
  inputData.clientId = req.user.userType == 'tango'? inputData?.clientId : req?.user?.clientId;
2387
2568
  // get openserach name form the env file
2388
2569
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2389
-
2570
+ const sortBy = inputData?.sortBy ? [ 'complianceScore', 'humanScore', 'trustScore', 'updatedAt', 'createdAt' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'RMName.keyword';
2390
2571
 
2391
2572
  let filter =[
2392
2573
  {
2393
2574
  'range': {
2394
2575
  'createdAt': {
2395
2576
  'gte': dayjs( inputData.fromDate ),
2396
- 'lte': dayjs( `${inputData.toDate} ${dayjs().format( 'HH:mm:ss' )}` ),
2577
+ 'lte': dayjs( inputData.toDate ),
2397
2578
  },
2398
2579
  },
2399
2580
  },
2400
2581
  ];
2401
2582
  let sort =[
2402
- { [inputData?.sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2583
+ { [sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2403
2584
  ];
2404
- if ( inputData.RmList && inputData.RmList?.lenght > 0 ) {
2405
- filter.push( {
2406
- 'terms': {
2407
- 'RMEmail.keyword': inputData?.RmList,
2585
+
2586
+ let search = {
2587
+ 'must': filter,
2588
+ 'should': [],
2589
+ 'minimum_should_match': 1,
2590
+ };
2591
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
2592
+ search ={
2593
+ 'must': filter,
2594
+ 'should': [
2595
+ {
2596
+ 'wildcard': {
2597
+ 'RMEmail.keyword': {
2598
+ 'value': `*${inputData.searchValue}*`,
2599
+ },
2600
+ },
2601
+ },
2602
+ {
2603
+ 'wildcard': {
2604
+ 'RMName.keyword': {
2605
+ 'value': `*${inputData.searchValue}*`,
2606
+ },
2607
+ },
2608
+ },
2609
+ {
2610
+ 'wildcard': {
2611
+ 'cluster.keyword': {
2612
+ 'value': `*${inputData.searchValue}*`,
2613
+ },
2614
+ },
2615
+ },
2616
+ {
2617
+ 'wildcard': {
2618
+ 'optumId.keyword': {
2619
+ 'value': `*${inputData.searchValue}*`,
2620
+ },
2621
+ },
2622
+ },
2623
+ {
2624
+ 'wildcard': {
2625
+ 'queueId.keyword': {
2626
+ 'value': `*${inputData.searchValue}*`,
2627
+ },
2628
+ },
2629
+ },
2630
+ {
2631
+ 'wildcard': {
2632
+ 'storeName.keyword': {
2633
+ 'value': `*${inputData.searchValue}*`,
2634
+ },
2635
+ },
2636
+ },
2637
+ {
2638
+ 'wildcard': {
2639
+ 'auditedBy.keyword': {
2640
+ 'value': `*${inputData.searchValue}*`,
2641
+ },
2642
+ },
2643
+ },
2644
+ {
2645
+ 'wildcard': {
2646
+ 'optomEmailId.keyword': {
2647
+ 'value': `*${inputData.searchValue}*`,
2648
+ },
2649
+ },
2650
+ },
2651
+ ],
2652
+
2653
+
2654
+ };
2655
+ }
2656
+
2657
+ if ( inputData.RMList && inputData.RMList?.length > 0 ) {
2658
+ const shouldRMQuery =inputData.RMList.map( ( email ) => ( {
2659
+ wildcard: {
2660
+ 'RMEmail.keyword': `*${email}*`,
2408
2661
  },
2409
- } );
2662
+ } ) );
2663
+ search['should'].push( ...shouldRMQuery );
2410
2664
  }
2411
2665
 
2412
- if ( inputData.cluster && inputData.cluster?.lenght > 0 ) {
2413
- filter.push( {
2414
- 'terms': {
2415
- 'cluster.keyword': inputData?.cluster,
2666
+ if ( inputData.cluster && inputData.cluster?.length > 0 ) {
2667
+ const shouldClusterQuery =inputData?.cluster?.map( ( cluster ) => ( {
2668
+ wildcard: {
2669
+ 'RMEmail.keyword': `*${cluster}*`,
2416
2670
  },
2417
- } );
2671
+ } ) );
2672
+ search['should'].push( ...shouldClusterQuery );
2418
2673
  }
2419
2674
  const getQuery = {
2420
2675
  'size': inputData.isExport == true? 50000 : inputData?.limit || 10,
2421
2676
  'from': offset || 0,
2422
2677
  'query': {
2423
- 'bool': {
2424
- 'must': filter,
2425
- },
2678
+ 'bool': search,
2426
2679
  },
2427
2680
  'sort': sort,
2428
2681
  };
2429
2682
 
2683
+
2430
2684
  // get query result from the open saerch
2431
2685
  const getData= await getOpenSearchData( openSearch.eyeTestEmailAlert, getQuery );
2686
+ const count = getData?.body?.hits?.total?.value;
2432
2687
  // throw error if got null or empty array
2433
2688
  if ( !getData || getData?.body?.hits?.hits?.length == 0 ) {
2434
2689
  return res.sendError( 'No records found', 204 );
@@ -2436,30 +2691,39 @@ export async function emailAlertLog( req, res ) {
2436
2691
  let response =[];
2437
2692
  for ( let item of getData?.body?.hits?.hits ) {
2438
2693
  response.push( {
2439
- RMEmail: item?._source?.RMEmail,
2694
+ RMEmail: item?._source?.RMEmail || '',
2695
+ RMName: item?._source?.RMName || '',
2440
2696
  cluster: item?._source?.cluster,
2441
2697
  optumId: item?._source?.optumId,
2698
+ optomEmailId: item?._source?.optomEmailId,
2442
2699
  storeName: item?._source?.storeName,
2443
- complianceScore: item?._source?.complianceScore,
2444
- trustScore: item?._source?.trustScore,
2700
+ complianceScore: Math.round( item?._source?.complianceScore || 0 ) + ' %',
2701
+ humanScore: Math.round( item?._source?.humanScore || 0 )+ ' %',
2702
+ trustScore: item?._source?.trustScore+ ' %' || '--',
2445
2703
  auditedId: item?._source?.auditedId,
2446
2704
  queueId: item?._source?.queueId,
2705
+ auditedBy: item?._source?.auditedBy || '--',
2447
2706
 
2448
2707
  } );
2449
2708
  }
2450
2709
 
2451
2710
  if ( inputData.isExport == true ) {
2711
+ logger.info( { response: response } );
2452
2712
  const chunkedMappingData = await chunkArray( response, 10 );
2453
2713
  const promises = chunkedMappingData.map( async ( chunk ) => {
2454
2714
  const exportData = [];
2455
2715
  for ( const element of chunk ) {
2456
2716
  exportData.push( {
2457
- 'RM Email': element.RMEmail,
2717
+ 'RM Name': element.RMEmail,
2458
2718
  'Cluster Name': element.cluster,
2459
- 'Optom Id': element.optumId,
2719
+ 'Optom ID': element.optumId,
2720
+ 'Optom Email': element.optomEmailId || '--',
2460
2721
  'Store Name': element.storeName,
2461
- 'Tango Score': element.complianceScore || 0,
2462
- 'Trust Score': element.trustScore || 0,
2722
+ 'Audited By': element.auditedBy || '--',
2723
+ 'Tango Score': Math.round( element.complianceScore || 0 )+' %',
2724
+ 'Trust Score': element.trustScore+' %' || '--',
2725
+ 'Human Audited Score': Math.round( element.humanScore || 0 ) + ' %',
2726
+
2463
2727
 
2464
2728
  } );
2465
2729
  }
@@ -2468,6 +2732,7 @@ export async function emailAlertLog( req, res ) {
2468
2732
  } );
2469
2733
  const mappedArrays = await Promise.all( promises );
2470
2734
  const result = mappedArrays.flat();
2735
+
2471
2736
  await download( result, res );
2472
2737
  return;
2473
2738
  }
@@ -2476,8 +2741,7 @@ export async function emailAlertLog( req, res ) {
2476
2741
  // throw error if null or undefined or empty array
2477
2742
 
2478
2743
 
2479
- logger.info( { inputData: inputData, getData: response } );
2480
- return res.sendSuccess( { result: response, count: response.length } );
2744
+ return res.sendSuccess( { result: response, count: count } );
2481
2745
  } catch ( error ) {
2482
2746
  const err = error.message || 'Internal Server Error';
2483
2747
  logger.error( { message: error, data: req.body, function: 'eyetestAudit-controller-emailAlertLog' } );
@@ -2637,10 +2901,6 @@ export async function getEmailConfig( req, res ) {
2637
2901
  configureType: 'email',
2638
2902
  };
2639
2903
  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
2904
  if ( !getOne ) {
2645
2905
  return res.sendError( ' No data found', 204 );
2646
2906
  }
@@ -1,4 +1,4 @@
1
- import { checkFileExist, chunkArray, download, getDate, getDateWithCustomizeTime, getJsonFileData, getObject, getOpenSearchData, getUTC, insertOpenSearchData, listFileByPath, logger, signedUrl, sqsReceive, updateOpenSearchData, sendMessageToQueue, listQueue, copyImage, upsertOpenSearchData } from 'tango-app-api-middleware';
1
+ import { checkFileExist, chunkArray, download, getDate, getDateWithCustomizeTime, getJsonFileData, getObject, getOpenSearchData, getUTC, insertOpenSearchData, listFileByPath, logger, signedUrl, sqsReceive, updateOpenSearchData, sendMessageToQueue, listQueue, copyImage } from 'tango-app-api-middleware';
2
2
  import { aggregateBinaryAudit, findOneBinaryAudit, updateOneBinaryAuditModel, updateOneUpsertBinaryAudit } from '../service/binaryAudit.service.js';
3
3
  import { countDocumentsStore, findOneStore } from '../service/store.service.js';
4
4
  import { findOneUser } from '../service/user.service.js';
@@ -1149,8 +1149,7 @@ export async function saveBinary( req, res ) {
1149
1149
  fileDetails: getTraxInsertData?.sqs?.Body?.fileDetails,
1150
1150
  createdAt: new Date(),
1151
1151
  };
1152
- const customerId = `${inputData.storeId}_${inputData.fileDate}_${inputData.moduleType}_${inputData.tempId}`;
1153
- await upsertOpenSearchData( openSearch.traxDetectionOutput, customerId, { doc: OpenSearchOutput } );
1152
+ await insertOpenSearchData( openSearch.traxDetectionOutput, OpenSearchOutput );
1154
1153
  const logData2 = {
1155
1154
  userId: req.user._id,
1156
1155
  userName: req.user.userName,
@@ -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,205 @@ 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
+ export async function sendComplianceEmail( {
68
+ toEmail,
69
+ fromEmail,
70
+ userName,
71
+ storeName,
72
+ startTime,
73
+ compliancePercentage,
74
+ steps,
75
+ videoUrl, // S3 cdn video URL
76
+ } ) {
77
+ // Initialize AWS SES client
78
+ const sesClient = new SESClient( {
79
+ region: 'ap-south-1',
80
+ credentials: {
81
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID_SIGNED_URL,
82
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY__SIGNED_URL,
83
+ },
84
+ } );
85
+
86
+ // Fetch video as base64 to attach in SES
87
+ const videoResponse = await fetch( videoUrl );
88
+ const videoBuffer = await videoResponse.arrayBuffer();
89
+ const videoBase64 = Buffer.from( videoBuffer ).toString( 'base64' );
90
+
91
+ // HTML Email Template
92
+ const htmlBody = ( userName, steps, compliancePercentage ) => {
93
+ const tableRows = steps
94
+ .map(
95
+ ( item ) => `
96
+ <tr>
97
+ <td>${item.stepName}</td>
98
+ <td>${item.comment ?? '--'}</td>
99
+ </tr>
100
+ `,
101
+ )
102
+ .join( '' );
103
+
104
+ return `
105
+ <!DOCTYPE html>
106
+ <html>
107
+ <head>
108
+ <meta charset="UTF-8" />
109
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
110
+ <title>Non Compliance Report</title>
111
+ <style>
112
+ body { font-family: 'Inter', sans-serif; margin: 0; padding: 0; background: #f9fafb; color: #121a26; }
113
+ .container { max-width: 700px; margin: 24px auto; background: #fff; padding: 24px; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
114
+ .header { display: flex; align-items: center; gap: 10px; }
115
+ .logo { width: 36px; height: 36px; border-radius: 50%; }
116
+ .title { font-size: 20px; font-weight: 700; color: #121a26; }
117
+ .subtitle { font-size: 15px; color: #384860; margin-top: 8px; line-height: 1.6; }
118
+ .divider { height: 2px; background: #eaeef0; margin: 16px 0; }
119
+ .table { width: 100%; border-collapse: collapse; margin-top: 16px; }
120
+ .table th, .table td { text-align: left; padding: 12px 16px; border-bottom: 1px solid #eaeef0; color: #384860; font-size: 14px; }
121
+ .compliance-box {
122
+ background: #F1F5F9;
123
+ color: #F04438;
124
+ font-weight: 700;
125
+ font-size: 18px;
126
+ padding: 12px 16px;
127
+ border-radius: 8px;
128
+ text-align: left;
129
+ margin-top: 24px;
130
+ border: 1px solid #e2e8f0; /* subtle border for better definition */
131
+ }
132
+ .video {
133
+ margin-top: 20px;
134
+ border-radius: 8px;
135
+ overflow: hidden;
136
+ }
137
+ .video video {
138
+ width: 100%;
139
+ border-radius: 8px;
140
+ }
141
+ a.button {
142
+ display: inline-block;
143
+ margin-top: 16px;
144
+ padding: 10px 16px;
145
+ background: #2563eb;
146
+ color: #fff;
147
+ text-decoration: none;
148
+ border-radius: 6px;
149
+ font-weight: 600;
150
+ }
151
+ .footer { margin-top: 24px; color: #555; font-size: 16px; text-align: left; }
152
+ .attachment { margin-top: 24px; color: #555; font-size: 18px; text-align: left; }
153
+ .copy-rights { margin-top: 24px; color: #555; font-size: 12px; text-align: left; }
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <div class="container">
158
+ <div class="header">
159
+ <img src="https://dashboard-api.tangoeye.ai/logo.png" alt="Team Tango" />
160
+
161
+ </div>
162
+
163
+ <div class="divider"></div>
164
+
165
+ <p class="subtitle">
166
+ Dear ${userName},<br><br>
167
+ Please ensure that all conducted eye tests meet the required compliance standards.<br><br>
168
+ Your prompt attention to this matter will help us maintain the highest standards and avoid any<br>
169
+ discrepancies. Please take a look at the below findings.
170
+ </p>
171
+
172
+ <table class="table">
173
+ <thead>
174
+ <tr>
175
+ <th>Step Name</th>
176
+ <th>Comments</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody>
180
+ ${tableRows}
181
+ </tbody>
182
+ </table>
183
+
184
+ <div class="compliance-box">Compliance Percentage: ${compliancePercentage}%</div>
185
+
186
+ <p class="attachment">One Attachment</p>
187
+ <!-- Video Embed (only if provided) -->
188
+ ${
189
+ videoUrl ?
190
+ `
191
+ <div class="video">
192
+ <a href="${videoUrl}" target="_blank" style="text-decoration:none; color:#00A3FF; font-weight:600;">
193
+ <img src="https://img.icons8.com/?size=100&id=63671&format=png&color=000000"
194
+ alt="Play Video" width="80" height="80"
195
+ style="vertical-align:middle; margin-right:10px;">
196
+ ${storeName}_${startTime}-video.mp4
197
+ </a>
198
+ </div>
199
+ ` :
200
+ ''
201
+ }
202
+
203
+ <p class="footer">Regards,<br>Team Tango</p>
204
+ </br></br>
205
+ <p class = "copy-rights">2024 © Tango Eye . All rights reserved.</p>
206
+ </div>
207
+
208
+ </body>
209
+ </html>`;
210
+ };
211
+
212
+ const emailHTML = htmlBody( userName, steps, compliancePercentage );
213
+
214
+ // Send email with video attachment
215
+ const params = {
216
+ Source: fromEmail,
217
+ Destination: {
218
+ ToAddresses: [ toEmail ],
219
+ },
220
+ Message: {
221
+ Subject: { Data: 'Non Compliance on Eye Test Procedures' },
222
+ Body: { Html: { Data: emailHTML } },
223
+ },
224
+ Attachments: [
225
+ {
226
+ Name: `${storeName}_${startTime}-video.mp4`,
227
+ Content: videoBase64,
228
+ ContentType: 'video/mp4',
229
+ },
230
+ ],
231
+ };
232
+
233
+ try {
234
+ const command = new SendEmailCommand( params );
235
+ const response = await sesClient.send( command );
236
+ logger.info( { response: response, command: command, params: params, code: response?.$metadata?.httpStatusCode } );
237
+ return response;
238
+ } catch ( error ) {
239
+ logger.error( 'Error sending compliance email', error );
240
+ throw error;
241
+ }
242
+ }
243
+
244
+