tango-app-api-audit 3.6.27 → 3.6.28

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.28",
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,45 @@ 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 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
- case '>': if ( trustScore > value ) {
878
+ case '>': if ( humanScore > 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,29 @@ 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
+ storeName: inputUpdatedData?._source?.storeName,
902
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
903
+ compliancePercentage: humanScore,
904
+ steps: steps,
905
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
906
+ } );
869
907
  }
870
908
  break;
871
- case '<': if ( trustScore < value ) {
909
+ case '<': if ( humanScore < value ) {
872
910
  const record2 = {
873
911
  RMEmail: inputData.RMEmail,
912
+ RMName: inputData.RMEmail,
874
913
  cluster: inputData.cluster,
875
914
  optumId: inputUpdatedData?._source?.optumId,
915
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
876
916
  storeName: inputUpdatedData?._source?.storeName,
877
917
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
918
+ humanScore: humanScore,
919
+ auditedBy: userName?.userName || '',
878
920
  trustScore: inputUpdatedData?._source?.trustScore,
879
921
  auditedId: inputUpdatedData?._id,
880
922
  queueId: inputUpdatedData?._source?.queueId,
@@ -883,15 +925,29 @@ export async function save( req, res ) {
883
925
 
884
926
  };
885
927
  await insertWithId( openSearch.eyeTestEmailAlert, id, record2 );
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
+ } );
886
938
  };
887
939
  break;
888
- case '>=': if ( trustScore >= value ) {
940
+ case '>=': if ( humanScore >= value ) {
889
941
  const record3 = {
890
942
  RMEmail: inputData.RMEmail,
943
+ RMName: inputData.RMEmail,
891
944
  cluster: inputData.cluster,
892
945
  optumId: inputUpdatedData?._source?.optumId,
946
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
893
947
  storeName: inputUpdatedData?._source?.storeName,
894
948
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
949
+ humanScore: humanScore,
950
+ auditedBy: userName?.userName || '',
895
951
  trustScore: inputUpdatedData?._source?.trustScore,
896
952
  auditedId: inputUpdatedData?._id,
897
953
  queueId: inputUpdatedData?._source?.queueId,
@@ -900,15 +956,29 @@ export async function save( req, res ) {
900
956
 
901
957
  };
902
958
  await insertWithId( openSearch.eyeTestEmailAlert, id, record3 );
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
+ } );
903
969
  }
904
970
  break;
905
- case '<=': if ( trustScore <= value ) {
971
+ case '<=': if ( humanScore <= value ) {
906
972
  const record4 = {
907
973
  RMEmail: inputData.RMEmail,
974
+ RMName: inputData.RMEmail,
908
975
  cluster: inputData.cluster,
909
976
  optumId: inputUpdatedData?._source?.optumId,
977
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
910
978
  storeName: inputUpdatedData?._source?.storeName,
911
979
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
980
+ humanScore: humanScore,
981
+ auditedBy: userName?.userName || '',
912
982
  trustScore: inputUpdatedData?._source?.trustScore,
913
983
  auditedId: inputUpdatedData?._id,
914
984
  queueId: inputUpdatedData?._source?.queueId,
@@ -917,16 +987,30 @@ export async function save( req, res ) {
917
987
 
918
988
  };
919
989
  await insertWithId( openSearch.eyeTestEmailAlert, id, record4 );
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
+ } );
920
1000
  }
921
1001
  break;
922
1002
  case '=':
923
- case '==': if ( trustScore == value ) {
1003
+ case '==': if ( humanScore == value ) {
924
1004
  const record5 = {
925
1005
  RMEmail: inputData.RMEmail,
1006
+ RMName: inputData.RMEmail,
926
1007
  cluster: inputData.cluster,
927
1008
  optumId: inputUpdatedData?._source?.optumId,
1009
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
928
1010
  storeName: inputUpdatedData?._source?.storeName,
929
1011
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1012
+ humanScore: humanScore,
1013
+ auditedBy: userName?.userName || '',
930
1014
  trustScore: inputUpdatedData?._source?.trustScore,
931
1015
  auditedId: inputUpdatedData?._id,
932
1016
  queueId: inputUpdatedData?._source?.queueId,
@@ -935,15 +1019,29 @@ export async function save( req, res ) {
935
1019
 
936
1020
  };
937
1021
  await insertWithId( openSearch.eyeTestEmailAlert, id, record5 );
1022
+ sendComplianceEmail( {
1023
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1024
+ fromEmail: ses.eyeTestAlertEmail,
1025
+ userName: 'User',
1026
+ storeName: inputUpdatedData?._source?.storeName,
1027
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
1028
+ compliancePercentage: humanScore,
1029
+ steps: steps,
1030
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
1031
+ } );
938
1032
  }
939
1033
  break;
940
- case '===': if ( trustScore === value ) {
1034
+ case '===': if ( humanScore === value ) {
941
1035
  const record6 = {
942
1036
  RMEmail: inputData.RMEmail,
1037
+ RMName: inputData.RMEmail,
943
1038
  cluster: inputData.cluster,
944
1039
  optumId: inputUpdatedData?._source?.optumId,
1040
+ optomEmailId: inputUpdatedData?._source?.salesmanEmailId,
945
1041
  storeName: inputUpdatedData?._source?.storeName,
946
1042
  complianceScore: inputUpdatedData?._source?.ComplianceScore,
1043
+ humanScore: humanScore,
1044
+ auditedBy: userName?.userName || '',
947
1045
  trustScore: inputUpdatedData?._source?.trustScore,
948
1046
  auditedId: inputUpdatedData?._id,
949
1047
  queueId: inputUpdatedData?._source?.queueId,
@@ -951,12 +1049,22 @@ export async function save( req, res ) {
951
1049
  updatedAt: new Date(),
952
1050
  };
953
1051
  await insertWithId( openSearch.eyeTestEmailAlert, id, record6 );
1052
+ sendComplianceEmail( {
1053
+ toEmail: inputUpdatedData?._source?.salesmanEmailId,
1054
+ fromEmail: ses.eyeTestAlertEmail,
1055
+ userName: 'User',
1056
+ storeName: inputUpdatedData?._source?.storeName,
1057
+ startTime: `${inputUpdatedData?._source?.fileDate}T${inputUpdatedData?._source.testEndTime}`,
1058
+ compliancePercentage: humanScore,
1059
+ steps: steps,
1060
+ videoUrl: `${url.physical}${inputUpdatedData?._source?.filePath}`,
1061
+ } );
954
1062
  }
955
1063
  break;
956
1064
  default: throw new Error( `Unknown condition: ${condition}` );
957
1065
  }
958
1066
  }
959
- return res.sendSuccess( 'The file has been submited successfully' );
1067
+ return res.sendSuccess( 'The file has been submitted successfully' );
960
1068
  } catch ( error ) {
961
1069
  const err = error.message || 'Internal Server Error';
962
1070
  logger.error( { message: error, data: req.params, function: 'eyetestAudit-controller-save' } );
@@ -1198,18 +1306,25 @@ export async function userAuditedData( req, res ) {
1198
1306
 
1199
1307
  export async function summaryList( req, res ) {
1200
1308
  try {
1309
+ // declare inputData and get request body
1201
1310
  const inputData = req.body;
1311
+
1312
+ // throw 204 if client id is not 11
1202
1313
  if ( inputData.clientId !== '11' ) {
1203
1314
  return res.sendError( 'No data found', 204 );
1204
1315
  }
1205
1316
 
1317
+ // declare openSearch and get data form the .env/secret manager
1206
1318
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1319
+
1320
+ // set limit, offset, sortBy, sortOrder value if not set default value
1207
1321
  const limit = inputData.limit || 10;
1208
1322
  const offset = inputData.offset ? ( inputData.offset - 1 ) * limit : 0;
1209
1323
 
1210
1324
  const sortBy = inputData?.sortBy ? [ 'coveredStepsAI', 'trustScore', 'date', 'ComplianceScore', 'auditedSteps' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'storeName.keyword';
1211
1325
  const order = inputData?.sortOrder || -1;
1212
1326
 
1327
+ // declare filter and assign range values from inputData
1213
1328
  let filter= [
1214
1329
  {
1215
1330
  'range': {
@@ -1222,6 +1337,7 @@ export async function summaryList( req, res ) {
1222
1337
 
1223
1338
  ];
1224
1339
 
1340
+ // filter by Audit Status if key has minimum one element
1225
1341
  if ( inputData?.auditStatus && inputData?.auditStatus?.length > 0 ) {
1226
1342
  filter.push( {
1227
1343
  'terms': {
@@ -1229,6 +1345,8 @@ export async function summaryList( req, res ) {
1229
1345
  },
1230
1346
  } );
1231
1347
  }
1348
+
1349
+ // filter by Visitors Type if key has minimum one element
1232
1350
  if ( inputData?.demographics && inputData?.demographics?.length > 0 ) {
1233
1351
  filter.push( {
1234
1352
  'terms': {
@@ -1236,6 +1354,8 @@ export async function summaryList( req, res ) {
1236
1354
  },
1237
1355
  } );
1238
1356
  }
1357
+
1358
+ // filter by Eye test Type if key has string value one element
1239
1359
  if ( inputData.type && inputData.type!='' ) {
1240
1360
  filter.push( {
1241
1361
  'term': {
@@ -1245,6 +1365,17 @@ export async function summaryList( req, res ) {
1245
1365
  } );
1246
1366
  }
1247
1367
 
1368
+ // filter by near addition if value != "" and eye test must be a physical
1369
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
1370
+ filter.push( {
1371
+ 'term': {
1372
+ 'nearAddition.keyword': inputData.nearAddition,
1373
+ },
1374
+
1375
+ } );
1376
+ }
1377
+
1378
+ // filter by stores if key has minimum one element else throw an error
1248
1379
  if ( inputData.storeId && inputData.storeId.length > 0 ) {
1249
1380
  filter.push( {
1250
1381
 
@@ -1257,11 +1388,14 @@ export async function summaryList( req, res ) {
1257
1388
  return res.sendError( 'No data found', 204 );
1258
1389
  }
1259
1390
 
1391
+ // Declare mustQuery as an array and include all elements from the filter array
1260
1392
  let mustQuery = [
1261
1393
  ...filter,
1262
1394
  ];
1263
1395
 
1264
- if ( inputData.complianceScore && inputData.complianceScore?.length > null ) {
1396
+ // filter by complianceScore/tango score if key has minimum one element
1397
+ if ( inputData.complianceScore && inputData.complianceScore?.length > 0 ) {
1398
+ // If the key type is not 'store', split the compliance score ranges
1265
1399
  if ( inputData.keyType !== 'store' ) {
1266
1400
  const rangeShould = inputData?.complianceScore?.map( ( r ) => {
1267
1401
  const [ gte, lte ] = r.split( '-' ).map( Number );
@@ -1271,7 +1405,7 @@ export async function summaryList( req, res ) {
1271
1405
  },
1272
1406
  };
1273
1407
  } );
1274
-
1408
+ // Add the range conditions to mustQuery (any one of the ranges should match)
1275
1409
  mustQuery.push(
1276
1410
  {
1277
1411
  bool: {
@@ -1280,7 +1414,7 @@ export async function summaryList( req, res ) {
1280
1414
  },
1281
1415
  },
1282
1416
  );
1283
- } else {
1417
+ } else { // If-else condition: use 'terms' filter to match records by Compliance Score / Tango Score
1284
1418
  const values = Array.isArray( inputData.complianceScore ) ?
1285
1419
  inputData.complianceScore.map( Number ) :
1286
1420
  [ Number( inputData.complianceScore ) ];
@@ -1293,6 +1427,7 @@ export async function summaryList( req, res ) {
1293
1427
  }
1294
1428
  }
1295
1429
 
1430
+ // filter by category if key exist allow "fake, genuine" else throw valid category
1296
1431
  if ( inputData?.category ) {
1297
1432
  switch ( inputData?.category ) {
1298
1433
  case 'fake':
@@ -1898,7 +2033,9 @@ export async function summaryList( req, res ) {
1898
2033
  'Store ID': element?._source?.storeId || 'Un Assigned',
1899
2034
  'Store Name': element?._source?.storeName ||'Un Assigned',
1900
2035
  'Optom ID': element?._source?.optumId,
2036
+ 'Optom Email ID': element?._source?.salesmanEmailId,
1901
2037
  'Queue ID': element?._source?.queueId,
2038
+ 'Power Types': element?._source?.nearAddition === 'true' ? 'With Near Addition' : element?._source?.nearAddition === 'false' ? 'Without Near Addition':'',
1902
2039
  'Tango Score': `${element?._source?.totalSteps>0 ? Math.round( ( element?._source?.coveredStepsAI/ element?._source?.totalSteps )*100 ): 0} %`,
1903
2040
  'Steps Covered By AI': element?._source?.coveredStepsAI || 0,
1904
2041
  '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 +2060,16 @@ export async function summaryList( req, res ) {
1923
2060
  'Store ID': element?._source?.storeId || 'Un Assigned',
1924
2061
  'Store Name': element?._source?.storeName ||'Un Assigned',
1925
2062
  'Engagement ID': element?._source?.engagementId,
2063
+ 'Optom ID': element?._source?.optm_userId,
2064
+ 'Optom Email ID': element?._source?.userEmail,
2065
+ 'Optom Name': element?._source?.displayName,
1926
2066
  '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
2067
  '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
2068
  'Trust Score': element?._source?.trustScore == null || element?._source?.trustScore ==undefined ? 'NA' : `${element?._source?.trustScore} %`,
2069
+ 'Steps Covered By Human': [ 'Re-Audited', 'Audited' ].includes( element?._source?.auditStatus )?element?._source?.auditedSteps :'NA',
2070
+ '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
2071
  'Test Duration': duration || '',
2072
+ 'Comments ': element._source?.subComment || '',
1932
2073
  'Audit Status': element?._source?.auditStatus || '',
1933
2074
  'Audited By': userName?.userName || '',
1934
2075
 
@@ -2118,6 +2259,15 @@ export async function summaryCard( req, res ) {
2118
2259
  },
2119
2260
  } );
2120
2261
  }
2262
+
2263
+ if ( inputData.nearAddition && inputData.nearAddition!='' && inputData.type ==='physical' ) {
2264
+ filter.push( {
2265
+ 'term': {
2266
+ 'nearAddition.keyword': inputData.nearAddition,
2267
+ },
2268
+
2269
+ } );
2270
+ }
2121
2271
  let mustQuery = [
2122
2272
  ...filter,
2123
2273
  ];
@@ -2271,7 +2421,7 @@ export async function summaryCard( req, res ) {
2271
2421
  'humanScore': {
2272
2422
  filter: {
2273
2423
  terms: {
2274
- 'auditStatus.keyword': [ 'Audited', 'ReAudited' ],
2424
+ 'auditStatus.keyword': [ 'Audited', 'Re-Audited' ],
2275
2425
  },
2276
2426
  },
2277
2427
  aggs: {
@@ -2386,7 +2536,7 @@ export async function emailAlertLog( req, res ) {
2386
2536
  inputData.clientId = req.user.userType == 'tango'? inputData?.clientId : req?.user?.clientId;
2387
2537
  // get openserach name form the env file
2388
2538
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2389
-
2539
+ const sortBy = inputData?.sortBy ? [ 'complianceScore', 'humanScore', 'trustScore', 'updatedAt', 'createdAt' ].includes( inputData.sortBy ) ? inputData.sortBy :`${inputData.sortBy}.keyword` : 'RMName.keyword';
2390
2540
 
2391
2541
  let filter =[
2392
2542
  {
@@ -2399,7 +2549,7 @@ export async function emailAlertLog( req, res ) {
2399
2549
  },
2400
2550
  ];
2401
2551
  let sort =[
2402
- { [inputData?.sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2552
+ { [sortBy]: { order: inputData.sortOrder === 1 ? 'asc' : 'desc' } },
2403
2553
  ];
2404
2554
  if ( inputData.RmList && inputData.RmList?.lenght > 0 ) {
2405
2555
  filter.push( {
@@ -2416,19 +2566,86 @@ export async function emailAlertLog( req, res ) {
2416
2566
  },
2417
2567
  } );
2418
2568
  }
2569
+ let search = {
2570
+ must: filter,
2571
+ };
2572
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
2573
+ search ={
2574
+ 'must': filter,
2575
+ 'should': [
2576
+ {
2577
+ 'wildcard': {
2578
+ 'RMEmail.keyword': {
2579
+ 'value': `*${inputData.searchValue}*`,
2580
+ },
2581
+ },
2582
+ },
2583
+ {
2584
+ 'wildcard': {
2585
+ 'RMName.keyword': {
2586
+ 'value': `*${inputData.searchValue}*`,
2587
+ },
2588
+ },
2589
+ },
2590
+ {
2591
+ 'wildcard': {
2592
+ 'cluster.keyword': {
2593
+ 'value': `*${inputData.searchValue}*`,
2594
+ },
2595
+ },
2596
+ },
2597
+ {
2598
+ 'wildcard': {
2599
+ 'optumId.keyword': {
2600
+ 'value': `*${inputData.searchValue}*`,
2601
+ },
2602
+ },
2603
+ },
2604
+ {
2605
+ 'wildcard': {
2606
+ 'queueId.keyword': {
2607
+ 'value': `*${inputData.searchValue}*`,
2608
+ },
2609
+ },
2610
+ },
2611
+ {
2612
+ 'wildcard': {
2613
+ 'storeName.keyword': {
2614
+ 'value': `*${inputData.searchValue}*`,
2615
+ },
2616
+ },
2617
+ },
2618
+ {
2619
+ 'wildcard': {
2620
+ 'auditedBy.keyword': {
2621
+ 'value': `*${inputData.searchValue}*`,
2622
+ },
2623
+ },
2624
+ },
2625
+ {
2626
+ 'wildcard': {
2627
+ 'optomEmailId.keyword': {
2628
+ 'value': `*${inputData.searchValue}*`,
2629
+ },
2630
+ },
2631
+ },
2632
+ ],
2633
+ 'minimum_should_match': 1,
2634
+ };
2635
+ }
2419
2636
  const getQuery = {
2420
2637
  'size': inputData.isExport == true? 50000 : inputData?.limit || 10,
2421
2638
  'from': offset || 0,
2422
2639
  'query': {
2423
- 'bool': {
2424
- 'must': filter,
2425
- },
2640
+ 'bool': search,
2426
2641
  },
2427
2642
  'sort': sort,
2428
2643
  };
2429
2644
 
2645
+
2430
2646
  // get query result from the open saerch
2431
2647
  const getData= await getOpenSearchData( openSearch.eyeTestEmailAlert, getQuery );
2648
+ const count = getData?.body?.hits?.total?.value;
2432
2649
  // throw error if got null or empty array
2433
2650
  if ( !getData || getData?.body?.hits?.hits?.length == 0 ) {
2434
2651
  return res.sendError( 'No records found', 204 );
@@ -2436,30 +2653,40 @@ export async function emailAlertLog( req, res ) {
2436
2653
  let response =[];
2437
2654
  for ( let item of getData?.body?.hits?.hits ) {
2438
2655
  response.push( {
2439
- RMEmail: item?._source?.RMEmail,
2656
+ RMEmail: item?._source?.RMEmail || '',
2657
+ RMName: item?._source?.RMName || '',
2440
2658
  cluster: item?._source?.cluster,
2441
2659
  optumId: item?._source?.optumId,
2660
+ optomEmailId: item?._source?.optomEmailId,
2442
2661
  storeName: item?._source?.storeName,
2443
- complianceScore: item?._source?.complianceScore,
2444
- trustScore: item?._source?.trustScore,
2662
+ complianceScore: Math.round( item?._source?.complianceScore || 0 ),
2663
+ humanScore: Math.round( item?._source?.humanScore || 0 ),
2664
+ trustScore: item?._source?.trustScore || '--',
2445
2665
  auditedId: item?._source?.auditedId,
2446
2666
  queueId: item?._source?.queueId,
2667
+ auditedBy: item?._source?.auditedBy || '--',
2447
2668
 
2448
2669
  } );
2449
2670
  }
2450
2671
 
2451
2672
  if ( inputData.isExport == true ) {
2673
+ logger.info( { response: response } );
2452
2674
  const chunkedMappingData = await chunkArray( response, 10 );
2453
2675
  const promises = chunkedMappingData.map( async ( chunk ) => {
2454
2676
  const exportData = [];
2455
2677
  for ( const element of chunk ) {
2456
2678
  exportData.push( {
2457
- 'RM Email': element.RMEmail,
2679
+ 'RM Name': element.RMEmail,
2458
2680
  'Cluster Name': element.cluster,
2459
- 'Optom Id': element.optumId,
2681
+ 'Optom ID': element.optumId,
2682
+ 'Optom Email': element.optomEmailId || '--',
2460
2683
  'Store Name': element.storeName,
2461
- 'Tango Score': element.complianceScore || 0,
2462
- 'Trust Score': element.trustScore || 0,
2684
+ 'Audited By': element.auditedBy || '--',
2685
+ 'Tango Score': Math.round( element.complianceScore || 0 ),
2686
+
2687
+ 'Trust Score': element.trustScore || '--',
2688
+ 'Human Audited Score': Math.round( element.humanScore || 0 ),
2689
+
2463
2690
 
2464
2691
  } );
2465
2692
  }
@@ -2468,6 +2695,7 @@ export async function emailAlertLog( req, res ) {
2468
2695
  } );
2469
2696
  const mappedArrays = await Promise.all( promises );
2470
2697
  const result = mappedArrays.flat();
2698
+ logger.info( { result: result } );
2471
2699
  await download( result, res );
2472
2700
  return;
2473
2701
  }
@@ -2477,7 +2705,7 @@ export async function emailAlertLog( req, res ) {
2477
2705
 
2478
2706
 
2479
2707
  logger.info( { inputData: inputData, getData: response } );
2480
- return res.sendSuccess( { result: response, count: response.length } );
2708
+ return res.sendSuccess( { result: response, count: count } );
2481
2709
  } catch ( error ) {
2482
2710
  const err = error.message || 'Internal Server Error';
2483
2711
  logger.error( { message: error, data: req.body, function: 'eyetestAudit-controller-emailAlertLog' } );
@@ -2637,10 +2865,6 @@ export async function getEmailConfig( req, res ) {
2637
2865
  configureType: 'email',
2638
2866
  };
2639
2867
  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
2868
  if ( !getOne ) {
2645
2869
  return res.sendError( ' No data found', 204 );
2646
2870
  }
@@ -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>
168
+ Your prompt attention to this matter will help us maintain the highest standards.<br>
169
+ 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
+