tango-app-api-payment-subscription 3.5.1 → 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.1",
3
+ "version": "3.5.2",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -481,25 +481,33 @@ export async function latestDailyPricing( req, res ) {
481
481
  const products = store.products || [];
482
482
  if ( products.length > 0 ) {
483
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';
484
490
  exportdata.push( {
485
491
  'Store Name': store.storeName,
486
492
  'Store ID': store.storeId,
487
493
  'Product': product.productName,
488
- 'Traffic Camera Count': store.trafficCameraCount || 0,
489
- 'Zone Camera Count': store.zoneCameraCount || 0,
490
- '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,
491
497
  'Working Days': product.workingdays || 0,
492
498
  'Status': store.status || '',
493
499
  } );
494
500
  } );
495
501
  } else {
502
+ // No product attached — leave all camera/zone columns at 0 so an
503
+ // unattributed row never carries product-specific counts.
496
504
  exportdata.push( {
497
505
  'Store Name': store.storeName,
498
506
  'Store ID': store.storeId,
499
507
  'Product': '',
500
- 'Traffic Camera Count': store.trafficCameraCount || 0,
501
- 'Zone Camera Count': store.zoneCameraCount || 0,
502
- 'Zone Count': store.zoneCount || 0,
508
+ 'Traffic Camera Count': 0,
509
+ 'Zone Camera Count': 0,
510
+ 'Zone Count': 0,
503
511
  'Working Days': 0,
504
512
  'Status': store.status || '',
505
513
  } );
@@ -1014,7 +1022,11 @@ export async function bulkDownloadBillingGroups( req, res ) {
1014
1022
 
1015
1023
  export async function bulkUpdateBillingGroups( req, res ) {
1016
1024
  try {
1017
- 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' ) {
1018
1030
  return res.sendError( 'No file uploaded', 400 );
1019
1031
  }
1020
1032
 
@@ -1023,9 +1035,22 @@ export async function bulkUpdateBillingGroups( req, res ) {
1023
1035
  return res.sendError( 'clientId form field is required', 400 );
1024
1036
  }
1025
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
+
1026
1051
  let rawRows;
1027
1052
  try {
1028
- const wb = XLSX.read( req.file.buffer, { type: 'buffer' } );
1053
+ const wb = XLSX.read( fileBuffer, { type: 'buffer' } );
1029
1054
  const sheet = wb.Sheets[wb.SheetNames[0]];
1030
1055
  rawRows = XLSX.utils.sheet_to_json( sheet, { defval: '' } );
1031
1056
  } catch ( parseErr ) {
@@ -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 } } },
@@ -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 );