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
|
@@ -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
|
-
|
|
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':
|
|
473
|
-
'Zone Camera Count':
|
|
474
|
-
'Zone Count':
|
|
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
|
-
'
|
|
750
|
-
'
|
|
751
|
-
'Invoice Receivers (count, read-only)',
|
|
782
|
+
'Store ID',
|
|
783
|
+
'Store Name (read-only)',
|
|
752
784
|
];
|
|
753
785
|
|
|
754
786
|
const COLUMN_WIDTHS = [
|
|
755
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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:
|
|
861
|
-
//
|
|
862
|
-
|
|
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
|
-
|
|
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(
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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:
|
|
1002
|
-
const
|
|
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.
|
|
1005
|
-
const foundDocs =
|
|
1006
|
-
await billingService.find( {
|
|
1118
|
+
.map( ( r ) => r.groupName ) ) );
|
|
1119
|
+
const foundDocs = groupNamesToCheck.length > 0 ?
|
|
1120
|
+
await billingService.find( { groupName: { $in: groupNamesToCheck }, clientId } ) :
|
|
1007
1121
|
[];
|
|
1008
|
-
const
|
|
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 ( !
|
|
1126
|
+
if ( !idByGroupName.has( row.groupName ) ) {
|
|
1013
1127
|
errors.push( {
|
|
1014
1128
|
rowNumber: i + 2,
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
//
|
|
1240
|
+
// Seventh pass: serial DB writes — one per billing group.
|
|
1034
1241
|
let updated = 0;
|
|
1035
1242
|
let aborted = null;
|
|
1036
|
-
for (
|
|
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
|
|
1250
|
+
clientId,
|
|
1047
1251
|
logSubType: 'billingGroupUpdate',
|
|
1048
1252
|
logType: 'billing',
|
|
1049
1253
|
date: new Date(),
|
|
1050
|
-
changes: [ `Billing group ${
|
|
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',
|
|
1057
|
-
aborted = { rowNumber:
|
|
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 ${
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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' ] } ] } ),
|
|
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,
|
|
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,
|
|
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,
|
|
21
|
-
invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler,
|
|
22
|
-
invoiceRouter.put( '/updateInvoice', isAllowedSessionHandler,
|
|
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,
|
|
37
|
+
invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteInvoice );
|
|
25
38
|
|
|
26
|
-
invoiceRouter.post( '/approveInvoiceCsm', isAllowedSessionHandler,
|
|
27
|
-
invoiceRouter.post( '/approveInvoiceFinance', isAllowedSessionHandler,
|
|
28
|
-
invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler,
|
|
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 );
|