tango-app-api-payment-subscription 3.5.0 → 3.5.2

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-payment-subscription",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import * as clientService from '../services/clientPayment.services.js';
2
2
  import * as invoiceService from '../services/invoice.service.js';
3
3
  import * as billingService from '../services/billing.service.js';
4
4
  import * as dailyPriceService from '../services/dailyPrice.service.js';
5
+ import * as storeService from '../services/store.service.js';
5
6
  import dayjs from 'dayjs';
6
7
  import { logger, checkFileExist, signedUrl, download, insertOpenSearchData } from 'tango-app-api-middleware';
7
8
  import * as XLSX from 'xlsx';
@@ -230,7 +231,10 @@ export async function brandInvoiceList( req, res ) {
230
231
  },
231
232
  } ];
232
233
 
233
- if ( req.body.durationFilter && req.body.durationFilter !== '' ) {
234
+ // If the user picked an explicit Month or Year, ignore durationFilter so the
235
+ // two ranges don't conflict (e.g. "current month" + April would always 0).
236
+ const hasMonthYear = ( req.body.monthFilter && String( req.body.monthFilter ) !== '' ) || ( req.body.yearFilter && String( req.body.yearFilter ) !== '' );
237
+ if ( !hasMonthYear && req.body.durationFilter && req.body.durationFilter !== '' ) {
234
238
  let dateFrom;
235
239
  const now = dayjs();
236
240
  if ( req.body.durationFilter === 'current' ) {
@@ -247,6 +251,30 @@ export async function brandInvoiceList( req, res ) {
247
251
  }
248
252
  }
249
253
 
254
+ // Month / Year filters (independent of durationFilter). Both are 1-based:
255
+ // monthFilter '1'..'12', yearFilter four-digit string like '2026'.
256
+ // billingDate may be stored as Date OR as a string in some legacy rows, so
257
+ // coerce to date inside the pipeline before $year/$month.
258
+ const monthNum = req.body.monthFilter ? parseInt( req.body.monthFilter, 10 ) : null;
259
+ const yearNum = req.body.yearFilter ? parseInt( req.body.yearFilter, 10 ) : null;
260
+ if ( ( monthNum && monthNum >= 1 && monthNum <= 12 ) || yearNum ) {
261
+ const billingDateExpr = {
262
+ $cond: [
263
+ { $eq: [ { $type: '$billingDate' }, 'date' ] },
264
+ '$billingDate',
265
+ { $toDate: '$billingDate' },
266
+ ],
267
+ };
268
+ const expr = { $and: [] };
269
+ if ( yearNum ) {
270
+ expr.$and.push( { $eq: [ { $year: billingDateExpr }, yearNum ] } );
271
+ }
272
+ if ( monthNum && monthNum >= 1 && monthNum <= 12 ) {
273
+ expr.$and.push( { $eq: [ { $month: billingDateExpr }, monthNum ] } );
274
+ }
275
+ query.push( { $match: { $expr: expr } } );
276
+ }
277
+
250
278
  if ( req.body.paymentStatus && req.body.paymentStatus.length > 0 ) {
251
279
  query.push( {
252
280
  $match: {
@@ -453,25 +481,33 @@ export async function latestDailyPricing( req, res ) {
453
481
  const products = store.products || [];
454
482
  if ( products.length > 0 ) {
455
483
  products.forEach( ( product ) => {
484
+ // Per-product zeroing: tangoTraffic rows only carry the traffic
485
+ // camera count; tangoZone rows only carry zone camera + zone count.
486
+ // Previously every row repeated all three values, which made
487
+ // tangoTraffic and tangoZone rows look identical in the export.
488
+ const isTraffic = product.productName === 'tangoTraffic';
489
+ const isZone = product.productName === 'tangoZone';
456
490
  exportdata.push( {
457
491
  'Store Name': store.storeName,
458
492
  'Store ID': store.storeId,
459
493
  'Product': product.productName,
460
- 'Traffic Camera Count': store.trafficCameraCount || 0,
461
- 'Zone Camera Count': store.zoneCameraCount || 0,
462
- 'Zone Count': store.zoneCount || 0,
494
+ 'Traffic Camera Count': isTraffic ? ( store.trafficCameraCount || 0 ) : 0,
495
+ 'Zone Camera Count': isZone ? ( store.zoneCameraCount || 0 ) : 0,
496
+ 'Zone Count': isZone ? ( store.zoneCount || 0 ) : 0,
463
497
  'Working Days': product.workingdays || 0,
464
498
  'Status': store.status || '',
465
499
  } );
466
500
  } );
467
501
  } else {
502
+ // No product attached — leave all camera/zone columns at 0 so an
503
+ // unattributed row never carries product-specific counts.
468
504
  exportdata.push( {
469
505
  'Store Name': store.storeName,
470
506
  'Store ID': store.storeId,
471
507
  'Product': '',
472
- 'Traffic Camera Count': store.trafficCameraCount || 0,
473
- 'Zone Camera Count': store.zoneCameraCount || 0,
474
- 'Zone Count': store.zoneCount || 0,
508
+ 'Traffic Camera Count': 0,
509
+ 'Zone Camera Count': 0,
510
+ 'Zone Count': 0,
475
511
  'Working Days': 0,
476
512
  'Status': store.status || '',
477
513
  } );
@@ -722,9 +758,6 @@ export async function getClientBillingInfo( req, res ) {
722
758
  }
723
759
 
724
760
  const COLUMN_HEADERS = [
725
- '_id (read-only)',
726
- 'Client ID (read-only)',
727
- 'Brand (read-only)',
728
761
  'Group Name',
729
762
  'Group Tag',
730
763
  'Registered Company Name',
@@ -735,7 +768,7 @@ const COLUMN_HEADERS = [
735
768
  'State',
736
769
  'Country',
737
770
  'Pin Code',
738
- 'Place Of Supply',
771
+ 'Place Of Supply (read-only)',
739
772
  'PO #',
740
773
  'Pro-Rata',
741
774
  'Payment Category',
@@ -746,15 +779,65 @@ const COLUMN_HEADERS = [
746
779
  'Is Installation One-Time',
747
780
  'Attach Annexure',
748
781
  'Advance Invoice',
749
- 'Is Primary (read-only)',
750
- 'Stores (count, read-only)',
751
- 'Invoice Receivers (count, read-only)',
782
+ 'Store ID',
783
+ 'Store Name (read-only)',
752
784
  ];
753
785
 
754
786
  const COLUMN_WIDTHS = [
755
- 28, 14, 24, 22, 12, 28, 18, 22, 22, 16, 16, 16, 12, 20, 14, 14, 18, 12, 16, 18, 16, 22, 18, 16, 18, 20, 28,
787
+ 22, 12, 28, 18, 22, 22, 16, 16, 16, 12, 22, 14, 14, 18, 12, 16, 18, 16, 22, 18, 16, 16, 28,
756
788
  ];
757
789
 
790
+ // GSTIN state-code prefix → Place of Supply (mirrors the UI map).
791
+ const GST_STATE_CODE_MAP = {
792
+ '01': 'Jammu and Kashmir',
793
+ '02': 'Himachal Pradesh',
794
+ '03': 'Punjab',
795
+ '04': 'Chandigarh',
796
+ '05': 'Uttarakhand',
797
+ '06': 'Haryana',
798
+ '07': 'Delhi',
799
+ '08': 'Rajasthan',
800
+ '09': 'Uttar Pradesh',
801
+ '10': 'Bihar',
802
+ '11': 'Sikkim',
803
+ '12': 'Arunachal Pradesh',
804
+ '13': 'Nagaland',
805
+ '14': 'Manipur',
806
+ '15': 'Mizoram',
807
+ '16': 'Tripura',
808
+ '17': 'Meghalaya',
809
+ '18': 'Assam',
810
+ '19': 'West Bengal',
811
+ '20': 'Jharkhand',
812
+ '21': 'Odisha',
813
+ '22': 'Chhattisgarh',
814
+ '23': 'Madhya Pradesh',
815
+ '24': 'Gujarat',
816
+ '25': 'Daman and Diu',
817
+ '26': 'Dadra and Nagar Haveli and Daman and Diu',
818
+ '27': 'Maharashtra',
819
+ '28': 'Andhra Pradesh (Old)',
820
+ '29': 'Karnataka',
821
+ '30': 'Goa',
822
+ '31': 'Lakshadweep',
823
+ '32': 'Kerala',
824
+ '33': 'Tamil Nadu',
825
+ '34': 'Puducherry',
826
+ '35': 'Andaman and Nicobar Islands',
827
+ '36': 'Telangana',
828
+ '37': 'Andhra Pradesh',
829
+ '38': 'Ladakh',
830
+ '97': 'Other Territory',
831
+ '99': 'Centre Jurisdiction',
832
+ };
833
+
834
+ function placeOfSupplyFromGst( gst ) {
835
+ if ( !gst ) return '';
836
+ const code = String( gst ).trim().toUpperCase().substring( 0, 2 );
837
+ const state = GST_STATE_CODE_MAP[code];
838
+ return state ? `${state} (${code})` : '';
839
+ }
840
+
758
841
  function boolToYesNo( v ) {
759
842
  return v === true ? 'Yes' : 'No';
760
843
  }
@@ -780,19 +863,21 @@ function numberOrUndefined( v ) {
780
863
  }
781
864
 
782
865
  function normalizeRow( rawRow ) {
866
+ const gst = trimOrEmpty( rawRow['GST'] );
783
867
  return {
784
- _id: trimOrEmpty( rawRow['_id (read-only)'] ),
785
868
  groupName: trimOrEmpty( rawRow['Group Name'] ),
786
869
  groupTag: trimOrEmpty( rawRow['Group Tag'] ),
787
870
  registeredCompanyName: trimOrEmpty( rawRow['Registered Company Name'] ),
788
- gst: trimOrEmpty( rawRow['GST'] ),
871
+ gst,
789
872
  addressLineOne: trimOrEmpty( rawRow['Address Line 1'] ),
790
873
  addressLineTwo: trimOrEmpty( rawRow['Address Line 2'] ),
791
874
  city: trimOrEmpty( rawRow['City'] ),
792
875
  state: trimOrEmpty( rawRow['State'] ),
793
876
  country: trimOrEmpty( rawRow['Country'] ),
794
877
  pinCode: trimOrEmpty( rawRow['Pin Code'] ),
795
- placeOfSupply: trimOrEmpty( rawRow['Place Of Supply'] ),
878
+ // Place of Supply is derived from the GST prefix on the server, ignoring
879
+ // whatever the cell holds (the column is read-only in the template).
880
+ placeOfSupply: placeOfSupplyFromGst( gst ),
796
881
  po: trimOrEmpty( rawRow['PO #'] ),
797
882
  proRata: trimOrEmpty( rawRow['Pro-Rata'] ),
798
883
  paymentCategory: trimOrEmpty( rawRow['Payment Category'] ),
@@ -803,6 +888,7 @@ function normalizeRow( rawRow ) {
803
888
  isInstallationOneTime: yesNoToBool( rawRow['Is Installation One-Time'] ),
804
889
  attachAnnexure: yesNoToBool( rawRow['Attach Annexure'] ),
805
890
  advanceInvoice: yesNoToBool( rawRow['Advance Invoice'] ),
891
+ storeId: trimOrEmpty( rawRow['Store ID'] ),
806
892
  };
807
893
  }
808
894
 
@@ -825,42 +911,60 @@ export async function bulkDownloadBillingGroups( req, res ) {
825
911
  { $sort: { groupName: 1 } },
826
912
  ] );
827
913
 
828
- const rows = groups.map( ( g ) => ( {
829
- [COLUMN_HEADERS[0]]: String( g._id || '' ),
830
- [COLUMN_HEADERS[1]]: g.clientId || '',
831
- [COLUMN_HEADERS[2]]: g._client?.clientName || '',
832
- [COLUMN_HEADERS[3]]: g.groupName || '',
833
- [COLUMN_HEADERS[4]]: g.groupTag || '',
834
- [COLUMN_HEADERS[5]]: g.registeredCompanyName || '',
835
- [COLUMN_HEADERS[6]]: g.gst || '',
836
- [COLUMN_HEADERS[7]]: g.addressLineOne || '',
837
- [COLUMN_HEADERS[8]]: g.addressLineTwo || '',
838
- [COLUMN_HEADERS[9]]: g.city || '',
839
- [COLUMN_HEADERS[10]]: g.state || '',
840
- [COLUMN_HEADERS[11]]: g.country || '',
841
- [COLUMN_HEADERS[12]]: g.pinCode || '',
842
- [COLUMN_HEADERS[13]]: g.placeOfSupply || '',
843
- [COLUMN_HEADERS[14]]: g.po || '',
844
- [COLUMN_HEADERS[15]]: g.proRata || '',
845
- [COLUMN_HEADERS[16]]: g.paymentCategory || '',
846
- [COLUMN_HEADERS[17]]: g.currency || '',
847
- [COLUMN_HEADERS[18]]: g.paymentCycle || '',
848
- [COLUMN_HEADERS[19]]: g.paymentTerm ?? '',
849
- [COLUMN_HEADERS[20]]: g.installationFee ?? '',
850
- [COLUMN_HEADERS[21]]: boolToYesNo( g.isInstallationOneTime ),
851
- [COLUMN_HEADERS[22]]: boolToYesNo( g.attachAnnexure ),
852
- [COLUMN_HEADERS[23]]: boolToYesNo( g.advanceInvoice ),
853
- [COLUMN_HEADERS[24]]: boolToYesNo( g.isPrimary ),
854
- [COLUMN_HEADERS[25]]: Array.isArray( g.stores ) ? g.stores.length : 0,
855
- [COLUMN_HEADERS[26]]: Array.isArray( g.generateInvoiceTo ) ? g.generateInvoiceTo.length : 0,
856
- } ) );
914
+ // Build storeId storeName lookup for this brand so we can show the name
915
+ // alongside the (editable) store id in the template.
916
+ const allStoreIds = new Set();
917
+ groups.forEach( ( g ) => {
918
+ if ( Array.isArray( g.stores ) ) g.stores.forEach( ( sid ) => allStoreIds.add( String( sid ) ) );
919
+ } );
920
+ const storeDocs = allStoreIds.size > 0 ?
921
+ await storeService.find( { clientId, storeId: { $in: Array.from( allStoreIds ) } }, { storeId: 1, storeName: 1 } ) :
922
+ [];
923
+ const storeNameById = new Map( storeDocs.map( ( s ) => [ String( s.storeId ), s.storeName || '' ] ) );
924
+
925
+ // Emit one row per store; group columns repeat. Groups with no stores
926
+ // still produce a single row (with empty store fields) so the user can
927
+ // see the group and add stores by editing the Store ID cell.
928
+ const rows = [];
929
+ for ( const g of groups ) {
930
+ const groupBase = {
931
+ [COLUMN_HEADERS[0]]: g.groupName || '',
932
+ [COLUMN_HEADERS[1]]: g.groupTag || '',
933
+ [COLUMN_HEADERS[2]]: g.registeredCompanyName || '',
934
+ [COLUMN_HEADERS[3]]: g.gst || '',
935
+ [COLUMN_HEADERS[4]]: g.addressLineOne || '',
936
+ [COLUMN_HEADERS[5]]: g.addressLineTwo || '',
937
+ [COLUMN_HEADERS[6]]: g.city || '',
938
+ [COLUMN_HEADERS[7]]: g.state || '',
939
+ [COLUMN_HEADERS[8]]: g.country || '',
940
+ [COLUMN_HEADERS[9]]: g.pinCode || '',
941
+ [COLUMN_HEADERS[10]]: g.placeOfSupply || placeOfSupplyFromGst( g.gst ),
942
+ [COLUMN_HEADERS[11]]: g.po || '',
943
+ [COLUMN_HEADERS[12]]: g.proRata || '',
944
+ [COLUMN_HEADERS[13]]: g.paymentCategory || '',
945
+ [COLUMN_HEADERS[14]]: g.currency || '',
946
+ [COLUMN_HEADERS[15]]: g.paymentCycle || '',
947
+ [COLUMN_HEADERS[16]]: g.paymentTerm ?? '',
948
+ [COLUMN_HEADERS[17]]: g.installationFee ?? '',
949
+ [COLUMN_HEADERS[18]]: boolToYesNo( g.isInstallationOneTime ),
950
+ [COLUMN_HEADERS[19]]: boolToYesNo( g.attachAnnexure ),
951
+ [COLUMN_HEADERS[20]]: boolToYesNo( g.advanceInvoice ),
952
+ };
953
+ const storeIds = Array.isArray( g.stores ) && g.stores.length > 0 ? g.stores : [ '' ];
954
+ for ( const sid of storeIds ) {
955
+ rows.push( {
956
+ ...groupBase,
957
+ [COLUMN_HEADERS[21]]: sid || '',
958
+ [COLUMN_HEADERS[22]]: sid ? ( storeNameById.get( String( sid ) ) || '' ) : '',
959
+ } );
960
+ }
961
+ }
857
962
 
858
963
  // Build the workbook with ExcelJS so cell protection is actually written
859
964
  // to the file (the community xlsx build strips per-cell styles on write).
860
- // Read-only columns by index: 0 (_id), 1 (Client ID), 2 (Brand),
861
- // 24 (Is Primary), 25 (Stores count),
862
- // 26 (Invoice Receivers count).
863
- const READ_ONLY_COLS = new Set( [ 0, 1, 2, 24, 25, 26 ] );
965
+ // Read-only columns by index: 10 (Place Of Supply derived from GST),
966
+ // 22 (Store Name).
967
+ const READ_ONLY_COLS = new Set( [ 10, 22 ] );
864
968
  const wb = new ExcelJS.Workbook();
865
969
  const ws = wb.addWorksheet( 'Billing Groups' );
866
970
  ws.columns = COLUMN_HEADERS.map( ( header, i ) => ( {
@@ -918,7 +1022,11 @@ export async function bulkDownloadBillingGroups( req, res ) {
918
1022
 
919
1023
  export async function bulkUpdateBillingGroups( req, res ) {
920
1024
  try {
921
- if ( !req.file?.buffer ) {
1025
+ // The frontend sends the XLSX as base64 in JSON (see billing.service.ts).
1026
+ // Switching off multipart sidestepped multer's "Unexpected end of form"
1027
+ // failures we were seeing in production.
1028
+ const fileBase64 = req.body?.fileBase64;
1029
+ if ( !fileBase64 || typeof fileBase64 !== 'string' ) {
922
1030
  return res.sendError( 'No file uploaded', 400 );
923
1031
  }
924
1032
 
@@ -927,9 +1035,22 @@ export async function bulkUpdateBillingGroups( req, res ) {
927
1035
  return res.sendError( 'clientId form field is required', 400 );
928
1036
  }
929
1037
 
1038
+ let fileBuffer;
1039
+ try {
1040
+ fileBuffer = Buffer.from( fileBase64, 'base64' );
1041
+ } catch ( decodeErr ) {
1042
+ logger.error( { error: decodeErr, function: 'bulkUpdateBillingGroups.decode' } );
1043
+ return res.sendError( 'Invalid file payload', 400 );
1044
+ }
1045
+
1046
+ // Match the 5MB cap multer was enforcing.
1047
+ if ( fileBuffer.length > 5 * 1024 * 1024 ) {
1048
+ return res.sendError( 'File too large (max 5MB)', 413 );
1049
+ }
1050
+
930
1051
  let rawRows;
931
1052
  try {
932
- const wb = XLSX.read( req.file.buffer, { type: 'buffer' } );
1053
+ const wb = XLSX.read( fileBuffer, { type: 'buffer' } );
933
1054
  const sheet = wb.Sheets[wb.SheetNames[0]];
934
1055
  rawRows = XLSX.utils.sheet_to_json( sheet, { defval: '' } );
935
1056
  } catch ( parseErr ) {
@@ -961,25 +1082,19 @@ export async function bulkUpdateBillingGroups( req, res ) {
961
1082
  }
962
1083
 
963
1084
  // Boolean cells that the user filled in but we couldn't parse → error (not silent skip).
964
- const rawIsOneTime = trimOrEmpty( raw[COLUMN_HEADERS[21]] );
1085
+ const rawIsOneTime = trimOrEmpty( raw['Is Installation One-Time'] );
965
1086
  if ( rawIsOneTime !== '' && row.isInstallationOneTime === undefined ) {
966
1087
  rowErrors.push( 'Is Installation One-Time must be Yes or No' );
967
1088
  }
968
- const rawAttachAnnexure = trimOrEmpty( raw[COLUMN_HEADERS[22]] );
1089
+ const rawAttachAnnexure = trimOrEmpty( raw['Attach Annexure'] );
969
1090
  if ( rawAttachAnnexure !== '' && row.attachAnnexure === undefined ) {
970
1091
  rowErrors.push( 'Attach Annexure must be Yes or No' );
971
1092
  }
972
- const rawAdvanceInvoice = trimOrEmpty( raw[COLUMN_HEADERS[23]] );
1093
+ const rawAdvanceInvoice = trimOrEmpty( raw['Advance Invoice'] );
973
1094
  if ( rawAdvanceInvoice !== '' && row.advanceInvoice === undefined ) {
974
1095
  rowErrors.push( 'Advance Invoice must be Yes or No' );
975
1096
  }
976
1097
 
977
- // Cross-brand guard: every row must belong to the brand the user is on.
978
- const rowClientId = trimOrEmpty( raw[COLUMN_HEADERS[1]] );
979
- if ( rowClientId !== '' && rowClientId !== clientId ) {
980
- rowErrors.push( `Row belongs to client ${rowClientId}, not ${clientId}. Upload only rows for the current brand.` );
981
- }
982
-
983
1098
  const { error: joiError } = bulkUpdateBillingGroupRowSchema.validate( row, { abortEarly: false, stripUnknown: true } );
984
1099
  if ( joiError ) {
985
1100
  for ( const d of joiError.details ) rowErrors.push( d.message );
@@ -988,8 +1103,7 @@ export async function bulkUpdateBillingGroups( req, res ) {
988
1103
  if ( rowErrors.length > 0 ) {
989
1104
  errors.push( {
990
1105
  rowNumber,
991
- _id: row._id,
992
- brand: raw[COLUMN_HEADERS[2]] || '',
1106
+ groupName: row.groupName,
993
1107
  errors: rowErrors,
994
1108
  } );
995
1109
  normalized.push( null );
@@ -998,28 +1112,121 @@ export async function bulkUpdateBillingGroups( req, res ) {
998
1112
  normalized.push( row );
999
1113
  }
1000
1114
 
1001
- // Second pass: verify each _id exists in the DB AND belongs to clientId.
1002
- const idsToCheck = normalized
1115
+ // Second pass: resolve each unique Group Name to a DB _id within this brand.
1116
+ const groupNamesToCheck = Array.from( new Set( normalized
1003
1117
  .filter( ( r ) => r !== null )
1004
- .map( ( r ) => r._id );
1005
- const foundDocs = idsToCheck.length > 0 ?
1006
- await billingService.find( { _id: { $in: idsToCheck }, clientId } ) :
1118
+ .map( ( r ) => r.groupName ) ) );
1119
+ const foundDocs = groupNamesToCheck.length > 0 ?
1120
+ await billingService.find( { groupName: { $in: groupNamesToCheck }, clientId } ) :
1007
1121
  [];
1008
- const foundIds = new Set( foundDocs.map( ( d ) => String( d._id ) ) );
1122
+ const idByGroupName = new Map( foundDocs.map( ( d ) => [ d.groupName, String( d._id ) ] ) );
1009
1123
  for ( let i = 0; i < normalized.length; i++ ) {
1010
1124
  const row = normalized[i];
1011
1125
  if ( !row ) continue;
1012
- if ( !foundIds.has( row._id ) ) {
1126
+ if ( !idByGroupName.has( row.groupName ) ) {
1013
1127
  errors.push( {
1014
1128
  rowNumber: i + 2,
1015
- _id: row._id,
1016
- brand: rawRows[i][COLUMN_HEADERS[2]] || '',
1017
- errors: [ 'Billing group not found' ],
1129
+ groupName: row.groupName,
1130
+ errors: [ `Billing group "${row.groupName}" not found for this brand` ],
1018
1131
  } );
1019
1132
  normalized[i] = null;
1020
1133
  }
1021
1134
  }
1022
1135
 
1136
+ // Third pass: aggregate rows by Group Name. Each group's stores[] is the
1137
+ // union of the Store ID cells across all of its rows. Group-level fields
1138
+ // are taken from the FIRST row for that group.
1139
+ const groupedByName = new Map(); // groupName → { _id, firstRowIndex, payload, storeIds: [] }
1140
+ for ( let i = 0; i < normalized.length; i++ ) {
1141
+ const row = normalized[i];
1142
+ if ( !row ) continue;
1143
+ const key = row.groupName;
1144
+ // eslint-disable-next-line no-unused-vars
1145
+ const { storeId, ...groupFields } = row;
1146
+ if ( !groupedByName.has( key ) ) {
1147
+ groupedByName.set( key, {
1148
+ _id: idByGroupName.get( key ),
1149
+ groupName: key,
1150
+ firstRowIndex: i,
1151
+ payload: groupFields,
1152
+ storeIds: [],
1153
+ storeIdSet: new Set(),
1154
+ } );
1155
+ }
1156
+ const entry = groupedByName.get( key );
1157
+ if ( storeId ) {
1158
+ if ( entry.storeIdSet.has( storeId ) ) {
1159
+ errors.push( {
1160
+ rowNumber: i + 2,
1161
+ groupName: key,
1162
+ errors: [ `Store ID ${storeId} is listed more than once for this billing group` ],
1163
+ } );
1164
+ continue;
1165
+ }
1166
+ entry.storeIdSet.add( storeId );
1167
+ entry.storeIds.push( storeId );
1168
+ }
1169
+ }
1170
+
1171
+ // Fourth pass: cross-group uniqueness — same Store ID may NOT appear in
1172
+ // two different groups in the upload.
1173
+ const storeIdToGroup = new Map(); // storeId → groupName
1174
+ for ( const entry of groupedByName.values() ) {
1175
+ for ( const sid of entry.storeIds ) {
1176
+ if ( storeIdToGroup.has( sid ) && storeIdToGroup.get( sid ) !== entry.groupName ) {
1177
+ errors.push( {
1178
+ rowNumber: entry.firstRowIndex + 2,
1179
+ groupName: entry.groupName,
1180
+ errors: [ `Store ID ${sid} is assigned to another billing group in this upload ("${storeIdToGroup.get( sid )}")` ],
1181
+ } );
1182
+ } else {
1183
+ storeIdToGroup.set( sid, entry.groupName );
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ // Fifth pass: store existence — every Store ID must exist for this clientId.
1189
+ const allStoreIds = Array.from( storeIdToGroup.keys() );
1190
+ const existingStoreDocs = allStoreIds.length > 0 ?
1191
+ await storeService.find( { clientId, storeId: { $in: allStoreIds } }, { storeId: 1 } ) :
1192
+ [];
1193
+ const existingStoreIds = new Set( existingStoreDocs.map( ( s ) => String( s.storeId ) ) );
1194
+ for ( const entry of groupedByName.values() ) {
1195
+ const missing = entry.storeIds.filter( ( sid ) => !existingStoreIds.has( String( sid ) ) );
1196
+ if ( missing.length > 0 ) {
1197
+ errors.push( {
1198
+ rowNumber: entry.firstRowIndex + 2,
1199
+ groupName: entry.groupName,
1200
+ errors: [ `Store ID${missing.length > 1 ? 's' : ''} ${missing.join( ', ' )} not found for this brand` ],
1201
+ } );
1202
+ }
1203
+ }
1204
+
1205
+ // Sixth pass: cross-group conflict with OTHER existing groups in the DB.
1206
+ // A store already assigned to a different billing group cannot be claimed
1207
+ // by this upload.
1208
+ const idsInUpload = Array.from( groupedByName.values() ).map( ( e ) => e._id );
1209
+ if ( allStoreIds.length > 0 ) {
1210
+ const conflictDocs = await billingService.find(
1211
+ { clientId, stores: { $in: allStoreIds }, _id: { $nin: idsInUpload } },
1212
+ { _id: 1, groupName: 1, stores: 1 },
1213
+ );
1214
+ for ( const doc of conflictDocs ) {
1215
+ const conflictIds = ( doc.stores || [] ).filter( ( sid ) => allStoreIds.includes( String( sid ) ) );
1216
+ for ( const sid of conflictIds ) {
1217
+ const claimingGroupName = storeIdToGroup.get( String( sid ) );
1218
+ const claimingEntry = groupedByName.get( claimingGroupName );
1219
+ if ( claimingEntry ) {
1220
+ errors.push( {
1221
+ rowNumber: claimingEntry.firstRowIndex + 2,
1222
+ groupName: claimingEntry.groupName,
1223
+ errors: [ `Store ID ${sid} is already assigned to billing group "${doc.groupName}"` ],
1224
+ } );
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+
1023
1230
  if ( errors.length > 0 ) {
1024
1231
  errors.sort( ( a, b ) => a.rowNumber - b.rowNumber );
1025
1232
  return res.sendError(
@@ -1030,31 +1237,28 @@ export async function bulkUpdateBillingGroups( req, res ) {
1030
1237
 
1031
1238
  const openSearchActivityLog = JSON.parse( process.env.OPENSEARCH ).activityLog;
1032
1239
 
1033
- // Third pass: serial DB writes.
1240
+ // Seventh pass: serial DB writes — one per billing group.
1034
1241
  let updated = 0;
1035
1242
  let aborted = null;
1036
- for ( let i = 0; i < normalized.length; i++ ) {
1037
- const row = normalized[i];
1038
- // eslint-disable-next-line no-unused-vars
1039
- const { _id, ...payload } = row;
1243
+ for ( const entry of groupedByName.values() ) {
1040
1244
  try {
1041
- await billingService.updateOne( { _id }, payload );
1245
+ await billingService.updateOne( { _id: entry._id }, { ...entry.payload, stores: entry.storeIds } );
1042
1246
  updated++;
1043
1247
  insertOpenSearchData( openSearchActivityLog, {
1044
1248
  userName: req.user?.userName,
1045
1249
  email: req.user?.email,
1046
- clientId: rawRows[i][COLUMN_HEADERS[1]] || '',
1250
+ clientId,
1047
1251
  logSubType: 'billingGroupUpdate',
1048
1252
  logType: 'billing',
1049
1253
  date: new Date(),
1050
- changes: [ `Billing group ${_id} updated via bulk upload` ],
1254
+ changes: [ `Billing group "${entry.groupName}" updated via bulk upload (stores=${entry.storeIds.length})` ],
1051
1255
  eventType: '',
1052
1256
  timestamp: new Date(),
1053
1257
  showTo: [ 'tango' ],
1054
1258
  } );
1055
1259
  } catch ( writeErr ) {
1056
- logger.error( { error: writeErr, function: 'bulkUpdateBillingGroups.write', rowNumber: i + 2, _id } );
1057
- aborted = { rowNumber: i + 2, _id, message: writeErr?.message || String( writeErr ) };
1260
+ logger.error( { error: writeErr, function: 'bulkUpdateBillingGroups.write', groupName: entry.groupName } );
1261
+ aborted = { rowNumber: entry.firstRowIndex + 2, groupName: entry.groupName, message: writeErr?.message || String( writeErr ) };
1058
1262
  break;
1059
1263
  }
1060
1264
  }
@@ -1066,7 +1270,7 @@ export async function bulkUpdateBillingGroups( req, res ) {
1066
1270
  logSubType: 'billingGroupBulkUpdate',
1067
1271
  logType: 'billing',
1068
1272
  date: new Date(),
1069
- changes: [ `Bulk billing-group update: ${updated} of ${normalized.length} groups updated by ${req.user?.email}` ],
1273
+ changes: [ `Bulk billing-group update: ${updated} of ${groupedByName.size} groups updated by ${req.user?.email}` ],
1070
1274
  eventType: '',
1071
1275
  timestamp: new Date(),
1072
1276
  showTo: [ 'tango' ],
@@ -1074,12 +1278,12 @@ export async function bulkUpdateBillingGroups( req, res ) {
1074
1278
 
1075
1279
  if ( aborted ) {
1076
1280
  return res.sendError(
1077
- { summary: { total: normalized.length, updated, remaining: normalized.length - updated }, aborted },
1281
+ { summary: { total: groupedByName.size, updated, remaining: groupedByName.size - updated }, aborted },
1078
1282
  207,
1079
1283
  );
1080
1284
  }
1081
1285
 
1082
- return res.sendSuccess( { summary: { total: normalized.length, updated } } );
1286
+ return res.sendSuccess( { summary: { total: groupedByName.size, updated } } );
1083
1287
  } catch ( error ) {
1084
1288
  logger.error( { error: error, function: 'bulkUpdateBillingGroups' } );
1085
1289
  return res.sendError( error, 500 );
@@ -174,6 +174,7 @@ export async function createInvoice( req, res ) {
174
174
  let invoicedate = req.body.invoiceId ? dayjs( findInvoice.billingDate ).format( 'YYYY-MM-DD' ) : baseDate.format( 'YYYY-MM-DD' );
175
175
  let daysExtend = group?.paymentTerm ? group?.paymentTerm : 30;
176
176
  let dueDate = baseDate.add( daysExtend, 'days' );
177
+ console.log( 'group.currencygroup.currency', group.currency );
177
178
  let data = {
178
179
  groupName: group.groupName,
179
180
  groupId: group._id,
@@ -265,6 +266,11 @@ export async function invoiceDownload( req, res ) {
265
266
  let invoiceInfo = await invoiceService.findOne( { _id: req.params.invoiceId } );
266
267
  if ( invoiceInfo ) {
267
268
  let clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
269
+ // The invoice records its own currency at creation time (from the
270
+ // billing group). That's the source of truth for the PDF — using
271
+ // client.paymentInvoice or virtualAccount.currency causes historical
272
+ // invoices to re-render in the wrong currency if those settings change.
273
+ const invoiceCurrency = symbolFor( invoiceInfo.currency );
268
274
  invoiceInfo.products.forEach( ( item, index ) => {
269
275
  item.index = index + 1;
270
276
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
@@ -272,7 +278,7 @@ export async function invoiceDownload( req, res ) {
272
278
  item.productName = firstWord + ' ' + secondWord;
273
279
  item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
274
280
  item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
275
- item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
281
+ item.currency = invoiceCurrency;
276
282
  } );
277
283
 
278
284
 
@@ -304,7 +310,7 @@ export async function invoiceDownload( req, res ) {
304
310
  PoNum: getgroup?.po,
305
311
  amountwords: AmountinWords,
306
312
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
307
- currencyType: symbolFor( virtualAccount?.currency ),
313
+ currencyType: invoiceCurrency,
308
314
  totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
309
315
  invoiceDate,
310
316
  dueDate,
@@ -375,7 +381,7 @@ export async function invoiceDownload( req, res ) {
375
381
  storeName: '$stores.storeName',
376
382
  edgefirstFileDate: '$stores.edgefirstFileDate',
377
383
  workingdays: '$stores.products.workingdays',
378
- currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
384
+ currencyType: { $literal: invoiceCurrency },
379
385
  },
380
386
  },
381
387
  {
@@ -580,6 +586,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
580
586
  }
581
587
 
582
588
  let clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
589
+ // Source of truth for the PDF currency is the invoice's own `currency`
590
+ // field, recorded at creation from the billing group. See invoiceDownload
591
+ // above for the same pattern.
592
+ const invoiceCurrency = symbolFor( invoiceInfo.currency );
583
593
  invoiceInfo.products.forEach( ( item, index ) => {
584
594
  item.index = index + 1;
585
595
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
@@ -587,7 +597,7 @@ async function buildInvoicePdfBuffer( invoiceId ) {
587
597
  item.productName = firstWord + ' ' + secondWord;
588
598
  item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
589
599
  item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
590
- item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
600
+ item.currency = invoiceCurrency;
591
601
  } );
592
602
 
593
603
  let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
@@ -616,7 +626,7 @@ async function buildInvoicePdfBuffer( invoiceId ) {
616
626
  PoNum: getgroup?.po,
617
627
  amountwords: AmountinWords,
618
628
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
619
- currencyType: symbolFor( virtualAccount?.currency ),
629
+ currencyType: invoiceCurrency,
620
630
  totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
621
631
  invoiceDate,
622
632
  dueDate,
@@ -661,7 +671,7 @@ async function buildInvoicePdfBuffer( invoiceId ) {
661
671
  storeName: '$stores.storeName',
662
672
  edgefirstFileDate: '$stores.edgefirstFileDate',
663
673
  workingdays: '$stores.products.workingdays',
664
- currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
674
+ currencyType: { $literal: invoiceCurrency },
665
675
  } },
666
676
  { $sort: { productName: 1, workingdays: -1 } },
667
677
  { $match: { workingdays: { $gt: 0 } } },
@@ -816,8 +826,10 @@ export async function invoiceAnnexure( req, res ) {
816
826
  return res.sendSuccess( { data: [] } );
817
827
  }
818
828
 
819
- const clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
820
829
  const currentMonthDays = dayjs().daysInMonth();
830
+ // Annexure must show the same currency as the invoice it accompanies —
831
+ // see invoiceDownload / buildInvoicePdfBuffer for the same pattern.
832
+ const invoiceCurrency = symbolFor( invoiceInfo.currency );
821
833
 
822
834
  const annexureData = await dailyPricingService.aggregate( [
823
835
  { $match: { clientId: invoiceInfo.clientId } },
@@ -832,7 +844,7 @@ export async function invoiceAnnexure( req, res ) {
832
844
  storeName: '$stores.storeName',
833
845
  edgefirstFileDate: '$stores.edgefirstFileDate',
834
846
  workingdays: '$stores.products.workingdays',
835
- currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
847
+ currencyType: { $literal: invoiceCurrency },
836
848
  } },
837
849
  { $sort: { productName: 1, workingdays: -1 } },
838
850
  { $match: { workingdays: { $gt: 0 } } },
@@ -2210,6 +2222,20 @@ export async function deleteInvoice( req, res ) {
2210
2222
 
2211
2223
  await invoiceService.deleteRecord( { _id: invoiceId } );
2212
2224
 
2225
+ const logObj = {
2226
+ userName: req.user?.userName,
2227
+ email: req.user?.email,
2228
+ clientId: invoice.clientId,
2229
+ logSubType: 'invoiceDeleted',
2230
+ logType: 'invoice',
2231
+ date: new Date(),
2232
+ changes: [ `Invoice ${invoice.invoice} has been deleted by ${req.user?.email}` ],
2233
+ eventType: 'delete',
2234
+ timestamp: new Date(),
2235
+ showTo: [ 'tango' ],
2236
+ };
2237
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
2238
+
2213
2239
  res.sendSuccess( { message: 'Invoice deleted successfully' } );
2214
2240
  } catch ( error ) {
2215
2241
  logger.error( { error: error, function: 'deleteInvoice' } );
@@ -372,7 +372,6 @@ export const updateBillingGroupsSchema = {
372
372
  };
373
373
 
374
374
  export const bulkUpdateBillingGroupRowSchema = joi.object( {
375
- _id: joi.string().length( 24 ).hex().required(),
376
375
  groupName: joi.string().required(),
377
376
  groupTag: joi.string().required(),
378
377
  registeredCompanyName: joi.string().required(),
@@ -394,6 +393,7 @@ export const bulkUpdateBillingGroupRowSchema = joi.object( {
394
393
  isInstallationOneTime: joi.boolean().optional(),
395
394
  attachAnnexure: joi.boolean().optional(),
396
395
  advanceInvoice: joi.boolean().optional(),
396
+ storeId: joi.string().allow( '' ).optional(),
397
397
  } );
398
398
 
399
399
  export const deleteBillingGroupQuery = {
@@ -1,21 +1,8 @@
1
1
 
2
2
  import express from 'express';
3
- import multer from 'multer';
4
3
  import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups } from '../controllers/brandsBilling.controller.js';
5
4
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
6
5
 
7
- const bulkUpload = multer( {
8
- storage: multer.memoryStorage(),
9
- limits: { fileSize: 5 * 1024 * 1024 },
10
- fileFilter: ( _req, file, cb ) => {
11
- const ok = [
12
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
13
- 'application/vnd.ms-excel',
14
- 'application/octet-stream',
15
- ].includes( file.mimetype );
16
- cb( ok ? null : new Error( 'Invalid file format — use XLSX' ), ok );
17
- },
18
- } );
19
6
  export const brandsBillingRouter = express.Router();
20
7
 
21
8
  brandsBillingRouter.post( '/brandsBillingList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), brandsBillingList );
@@ -26,4 +13,4 @@ brandsBillingRouter.put( '/updateDailyPricingWorkingDays', isAllowedSessionHandl
26
13
  brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingStoreField );
27
14
  brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getClientBillingInfo );
28
15
  brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
29
- brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpload.single( 'file' ), bulkUpdateBillingGroups );
16
+ brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpdateBillingGroups );
@@ -5,6 +5,19 @@ import { getInvoiceHeads, updateInvoiceHeads } from '../controllers/applicationD
5
5
  import { updateInvoiceHeadsSchema } from '../dtos/validation.dtos.js';
6
6
  export const invoiceRouter = express.Router();
7
7
 
8
+ // Superadmin bypass: if the authenticated user has role 'superadmin', skip the
9
+ // granular access check that follows. Place between isAllowedSessionHandler and
10
+ // accessVerification so req.user is populated.
11
+ function superadminBypass( accessConfig ) {
12
+ const verify = accessVerification( accessConfig );
13
+ return function( req, res, next ) {
14
+ if ( req.user && req.user.role === 'superadmin' ) {
15
+ return next();
16
+ }
17
+ return verify( req, res, next );
18
+ };
19
+ }
20
+
8
21
 
9
22
  invoiceRouter.post( '/createInvoice', isAllowedSessionHandler, createInvoice );
10
23
  invoiceRouter.post( '/regerateInvoice', isAllowedSessionHandler, createInvoice );
@@ -13,19 +26,19 @@ invoiceRouter.post( '/invoiceDownload/:invoiceId', isAllowedSessionHandler, invo
13
26
  invoiceRouter.post( '/clientInvoiceList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), clientInvoiceList );
14
27
  invoiceRouter.post( '/creditTransactionlist', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), creditTransactionlist );
15
28
  invoiceRouter.post( '/pendingInvoices', isAllowedSessionHandler, pendingInvoices );
16
- invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), applyDiscount );
29
+ invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), applyDiscount );
17
30
  invoiceRouter.post( '/migrateInvoice', migrateInvoice );
18
- invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
31
+ invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
19
32
  invoiceRouter.post( '/checkPaymentStatus', checkPaymentStatus );
20
- invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
21
- invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
22
- invoiceRouter.put( '/updateInvoice', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateInvoice );
33
+ invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
34
+ invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
35
+ invoiceRouter.put( '/updateInvoice', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateInvoice );
23
36
  invoiceRouter.get( '/getClientBasePricing/:clientId', isAllowedSessionHandler, getClientBasePricing );
24
- invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteInvoice );
37
+ invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteInvoice );
25
38
 
26
- invoiceRouter.post( '/approveInvoiceCsm', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'csmApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceCsm );
27
- invoiceRouter.post( '/approveInvoiceFinance', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'financeApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceFinance );
28
- invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceApproval );
39
+ invoiceRouter.post( '/approveInvoiceCsm', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'csmApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceCsm );
40
+ invoiceRouter.post( '/approveInvoiceFinance', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'financeApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceFinance );
41
+ invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceApproval );
29
42
 
30
43
  invoiceRouter.get( '/getInvoiceHeads', isAllowedSessionHandler, getInvoiceHeads );
31
44
  invoiceRouter.post( '/updateInvoiceHeads', isAllowedSessionHandler, validate( updateInvoiceHeadsSchema ), updateInvoiceHeads );