tango-app-api-payment-subscription 3.5.20 → 3.5.21

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.20",
3
+ "version": "3.5.21",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -13,7 +13,7 @@ import htmlpdf from 'html-pdf-node';
13
13
  import archiver from 'archiver';
14
14
  import * as basepricingService from '../services/basePrice.service.js';
15
15
  import * as paymentAccountService from '../services/paymentAccount.service.js';
16
- import { symbolFor } from '../utils/currency.js';
16
+ import { symbolFor, roundAmount, wordFor } from '../utils/currency.js';
17
17
  import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
18
18
  import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
19
19
  import * as assignedStoreService from '../services/assignedStore.service.js';
@@ -162,13 +162,14 @@ export async function createInvoice( req, res ) {
162
162
  }
163
163
  // Recompute totals from the (possibly expanded) line items so a
164
164
  // multi-month advance invoice bills the full period.
165
+ const customCurrency = req.body.currency || 'inr';
165
166
  const customAmount = customAdvanceMonths > 1 ?
166
- Math.round( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ) ) :
167
- Math.round( Number( req.body.amount ) || 0 );
167
+ roundAmount( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ), customCurrency ) :
168
+ roundAmount( Number( req.body.amount ) || 0, customCurrency );
168
169
  const customTotal = customAdvanceMonths > 1 ?
169
- Math.round( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ) +
170
- ( Array.isArray( req.body.tax ) ? req.body.tax.reduce( ( s, t ) => s + ( Number( t.taxAmount ) || 0 ) * customAdvanceMonths, 0 ) : 0 ) ) :
171
- Math.round( Number( req.body.totalAmount ) || 0 );
170
+ roundAmount( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ) +
171
+ ( Array.isArray( req.body.tax ) ? req.body.tax.reduce( ( s, t ) => s + ( Number( t.taxAmount ) || 0 ) * customAdvanceMonths, 0 ) : 0 ), customCurrency ) :
172
+ roundAmount( Number( req.body.totalAmount ) || 0, customCurrency );
172
173
 
173
174
  const data = {
174
175
  invoice: `${invPrefix}${Finacialyear}-${invoiceNo}`,
@@ -396,7 +397,7 @@ export async function createInvoice( req, res ) {
396
397
  productName: 'oneTimeFee',
397
398
  period: 'fullmonth',
398
399
  storeCount: newStoreCount,
399
- amount: Math.round( newStoreCount * oneTimeFeePerStore * 100 ) / 100,
400
+ amount: roundAmount( newStoreCount * oneTimeFeePerStore, group.currency ),
400
401
  price: oneTimeFeePerStore,
401
402
  description: `One-Time Fee - ${newStoreCount} stores`,
402
403
  HsnNumber: '998314',
@@ -417,21 +418,21 @@ export async function createInvoice( req, res ) {
417
418
  // existing GST/IGST/CGST/SGST behavior for legacy records that have
418
419
  // no taxCalculationType set yet.
419
420
  if ( group.taxCalculationType === 'international' ) {
420
- totalAmount = Math.round( amount );
421
+ totalAmount = roundAmount( amount, group.currency );
421
422
  } else if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
422
423
  let taxAmount = ( amount * 18 ) / 100;
423
- totalAmount = Math.round( amount + taxAmount );
424
+ totalAmount = roundAmount( amount + taxAmount, group.currency );
424
425
  taxList.push(
425
426
  {
426
427
  'currency': '₹',
427
428
  'type': 'CGST',
428
429
  'value': 9,
429
- 'taxAmount': ( ( amount * 9 ) / 100 ).toFixed( 2 ),
430
+ 'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
430
431
  }, {
431
432
  'currency': '₹',
432
433
  'type': 'SGST',
433
434
  'value': 9,
434
- 'taxAmount': ( ( amount * 9 ) / 100 ).toFixed( 2 ),
435
+ 'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
435
436
  },
436
437
  );
437
438
  } else {
@@ -439,13 +440,13 @@ export async function createInvoice( req, res ) {
439
440
  if ( group.currency === 'inr' ) {
440
441
  taxAmount = ( amount * 18 ) / 100;
441
442
  }
442
- totalAmount = Math.round( amount + taxAmount );
443
+ totalAmount = roundAmount( amount + taxAmount, group.currency );
443
444
  taxList.push(
444
445
  {
445
446
  'currency': '₹',
446
447
  'type': 'IGST',
447
448
  'value': 18,
448
- 'taxAmount': ( taxAmount ).toFixed( 2 ),
449
+ 'taxAmount': String( roundAmount( taxAmount, group.currency ) ),
449
450
  },
450
451
  );
451
452
  }
@@ -528,14 +529,14 @@ export async function createInvoice( req, res ) {
528
529
  invoice: req.body.invoiceId ? req.body.invoiceId : `${invPrefix}${Finacialyear}-${invoiceNo}`,
529
530
  products: products,
530
531
  status: 'pendingCsm',
531
- amount: Math.round( amount ),
532
+ amount: roundAmount( amount, group.currency ),
532
533
  invoiceIndex: req.body.invoiceId ? findInvoice.invoiceIndex : invoiceNo,
533
534
  tax: taxList,
534
535
  companyName: ( group.registeredCompanyName || '' ).toUpperCase(),
535
536
  companyAddress: address,
536
537
  PlaceOfSupply: group.placeOfSupply,
537
538
  GSTNumber: group.gst,
538
- totalAmount: Math.round( totalAmount ),
539
+ totalAmount: roundAmount( totalAmount, group.currency ),
539
540
  clientId: group.clientId,
540
541
  paymentMethod: 'Online',
541
542
  billingDate: new Date( invoicedate ),
@@ -751,8 +752,8 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
751
752
  // store's position within the product.
752
753
  const price = priceForStore( s.productName, productPosition[s.productName] );
753
754
  const runningCost = s.workingdays >= monthDays ?
754
- Math.round( price * units * 100 ) / 100 :
755
- Math.round( ( price / monthDays ) * s.workingdays * units * 100 ) / 100;
755
+ roundAmount( price * units, invoiceInfo.currency ) :
756
+ roundAmount( ( price / monthDays ) * s.workingdays * units, invoiceInfo.currency );
756
757
  return {
757
758
  productName: s.productName ? s.productName.charAt( 0 ).toUpperCase() + s.productName.slice( 1 ) : '',
758
759
  currencyType: invoiceCurrency,
@@ -770,8 +771,9 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
770
771
  };
771
772
  } );
772
773
 
773
- const totalAmount = Math.round( data.reduce( ( a, x ) => a + ( x.runningCost || 0 ), 0 ) * 100 ) / 100;
774
- const totalFormatted = totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
774
+ const totalAmount = roundAmount( data.reduce( ( a, x ) => a + ( x.runningCost || 0 ), 0 ), invoiceInfo.currency );
775
+ const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
776
+ const totalFormatted = totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits } );
775
777
  return { data, totalAmount, totalFormatted };
776
778
  }
777
779
 
@@ -786,20 +788,23 @@ export async function invoiceDownload( req, res ) {
786
788
  // client.paymentInvoice or virtualAccount.currency causes historical
787
789
  // invoices to re-render in the wrong currency if those settings change.
788
790
  const invoiceCurrency = symbolFor( invoiceInfo.currency );
791
+ // INR shows whole numbers; every other currency keeps 2 decimals.
792
+ const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
793
+ const moneyFmt = { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
789
794
  invoiceInfo.products.forEach( ( item, index ) => {
790
795
  item.index = index + 1;
791
796
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
792
797
  firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
793
798
  item.productName = firstWord + ' ' + secondWord;
794
- item.price = item.price .toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
795
- item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
799
+ item.price = roundAmount( item.price, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
800
+ item.amount = roundAmount( item.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
796
801
  item.currency = invoiceCurrency;
797
802
  } );
798
803
 
799
804
 
800
805
  let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
801
806
 
802
- invoiceInfo.totalAmount = Math.round( invoiceInfo.totalAmount );
807
+ invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
803
808
  let AmountinWords = inWords( invoiceInfo.totalAmount );
804
809
  let getgroup;
805
810
  let days = getgroup?.paymentTerm ? getgroup?.paymentTerm : '30';
@@ -814,10 +819,10 @@ export async function invoiceDownload( req, res ) {
814
819
  invoiceData = {
815
820
  ...invoiceInfo._doc,
816
821
  clientName: clientDetails.clientName,
817
- amount: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
822
+ amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
818
823
  extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
819
824
  address: clientDetails.billingDetails.billingAddress,
820
- subtotal: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
825
+ subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
821
826
  companyName: invoiceInfo.companyName,
822
827
  companyAddress: invoiceInfo.companyAddress,
823
828
  PlaceOfSupply: invoiceInfo.PlaceOfSupply,
@@ -826,7 +831,8 @@ export async function invoiceDownload( req, res ) {
826
831
  amountwords: AmountinWords,
827
832
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
828
833
  currencyType: invoiceCurrency,
829
- totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
834
+ currencyWord: wordFor( invoiceInfo.currency ),
835
+ totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
830
836
  invoiceDate,
831
837
  dueDate,
832
838
  discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
@@ -985,18 +991,21 @@ async function buildInvoicePdfBuffer( invoiceId ) {
985
991
  // field, recorded at creation from the billing group. See invoiceDownload
986
992
  // above for the same pattern.
987
993
  const invoiceCurrency = symbolFor( invoiceInfo.currency );
994
+ // INR shows whole numbers; every other currency keeps 2 decimals.
995
+ const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
996
+ const moneyFmt = { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
988
997
  invoiceInfo.products.forEach( ( item, index ) => {
989
998
  item.index = index + 1;
990
999
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
991
1000
  firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
992
1001
  item.productName = firstWord + ' ' + secondWord;
993
- item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
994
- item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
1002
+ item.price = roundAmount( item.price, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
1003
+ item.amount = roundAmount( item.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
995
1004
  item.currency = invoiceCurrency;
996
1005
  } );
997
1006
 
998
1007
  let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
999
- invoiceInfo.totalAmount = Math.round( invoiceInfo.totalAmount );
1008
+ invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
1000
1009
  let AmountinWords = inWords( invoiceInfo.totalAmount );
1001
1010
  let getgroup;
1002
1011
  if ( invoiceInfo.groupId ) {
@@ -1010,10 +1019,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
1010
1019
  let invoiceData = {
1011
1020
  ...invoiceInfo._doc,
1012
1021
  clientName: clientDetails.clientName,
1013
- amount: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
1022
+ amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
1014
1023
  extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
1015
1024
  address: clientDetails.billingDetails.billingAddress,
1016
- subtotal: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
1025
+ subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
1017
1026
  companyName: invoiceInfo.companyName,
1018
1027
  companyAddress: invoiceInfo.companyAddress,
1019
1028
  PlaceOfSupply: invoiceInfo.PlaceOfSupply,
@@ -1022,7 +1031,8 @@ async function buildInvoicePdfBuffer( invoiceId ) {
1022
1031
  amountwords: AmountinWords,
1023
1032
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
1024
1033
  currencyType: invoiceCurrency,
1025
- totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
1034
+ currencyWord: wordFor( invoiceInfo.currency ),
1035
+ totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
1026
1036
  invoiceDate,
1027
1037
  dueDate,
1028
1038
  discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
@@ -1638,7 +1648,7 @@ async function standardPrice( group, getClient, baseDate ) {
1638
1648
  if ( store.workingDays >= currentMonthDays ) {
1639
1649
  amount = store.price * storeCount;
1640
1650
  } else {
1641
- amount = Math.round( ( store.price / currentMonthDays ) * store.workingDays * storeCount * 100 ) / 100;
1651
+ amount = roundAmount( ( store.price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
1642
1652
  }
1643
1653
 
1644
1654
  let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
@@ -1837,7 +1847,7 @@ async function stepPrice( group, getClient ) {
1837
1847
  if ( store.workingDays >= currentMonthDays ) {
1838
1848
  amount = price * storeCount;
1839
1849
  } else {
1840
- amount = Math.round( ( price / currentMonthDays ) * store.workingDays * storeCount * 100 ) / 100;
1850
+ amount = roundAmount( ( price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
1841
1851
  }
1842
1852
 
1843
1853
  let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
@@ -1913,8 +1923,8 @@ async function stepPrice( group, getClient ) {
1913
1923
  const price = tierPriceForPosition( s.productName, productPosition[s.productName] );
1914
1924
  const fullMonth = s.workingDays >= currentMonthDays;
1915
1925
  const amount = fullMonth ?
1916
- Math.round( units * price * 100 ) / 100 :
1917
- Math.round( ( units * ( price / currentMonthDays ) * s.workingDays ) * 100 ) / 100;
1926
+ roundAmount( units * price, group.currency ) :
1927
+ roundAmount( units * ( price / currentMonthDays ) * s.workingDays, group.currency );
1918
1928
  const period = fullMonth ? 'fullMonth' : 'proRate';
1919
1929
  const key = `${s.productName}_${period}_${price}`;
1920
1930
  if ( !grouped[key] ) {
@@ -1931,7 +1941,8 @@ async function stepPrice( group, getClient ) {
1931
1941
  } else if ( grp.productName === 'tangoZone' ) {
1932
1942
  description = 'Product category/section analytics';
1933
1943
  }
1934
- const amount = Math.round( grp.totalAmount * 100 ) / 100;
1944
+ const amount = roundAmount( grp.totalAmount, group.currency );
1945
+ const zeroPrice = group.currency === 'inr' ? '0' : '0.00';
1935
1946
  return {
1936
1947
  productName: grp.productName,
1937
1948
  period: grp.period,
@@ -1943,7 +1954,7 @@ async function stepPrice( group, getClient ) {
1943
1954
  HsnNumber: '998314',
1944
1955
  amount: amount,
1945
1956
  month: dayjs().format( 'MMM YYYY' ),
1946
- price: grp.unitCount ? ( amount / grp.unitCount ).toFixed( 2 ) : '0.00',
1957
+ price: grp.unitCount ? String( roundAmount( amount / grp.unitCount, group.currency ) ) : zeroPrice,
1947
1958
  };
1948
1959
  } );
1949
1960
 
@@ -2229,9 +2240,11 @@ export async function clientInvoiceList( req, res ) {
2229
2240
  cards.pendingPaymentCount += 1;
2230
2241
  }
2231
2242
  }
2232
- cards.outstandingAmount = Math.round( cards.outstandingAmount * 100 ) / 100;
2233
- cards.overdueAmount = Math.round( cards.overdueAmount * 100 ) / 100;
2234
- cards.pendingPaymentAmount = Math.round( cards.pendingPaymentAmount * 100 ) / 100;
2243
+ // These card totals are normalised to INR (dollar invoices are multiplied
2244
+ // by usdRate above), so they follow the INR rule: whole numbers.
2245
+ cards.outstandingAmount = roundAmount( cards.outstandingAmount, 'inr' );
2246
+ cards.overdueAmount = roundAmount( cards.overdueAmount, 'inr' );
2247
+ cards.pendingPaymentAmount = roundAmount( cards.pendingPaymentAmount, 'inr' );
2235
2248
 
2236
2249
  res.sendSuccess( { count: count.length, data: invoiceList, cards } );
2237
2250
  } catch ( error ) {
@@ -2363,14 +2376,14 @@ export async function applyDiscount( req, res ) {
2363
2376
  try {
2364
2377
  let invoice = await invoiceService.findOne( { invoice: req.body.invoice } );
2365
2378
  if ( invoice ) {
2366
- invoice.discountAmount = ( ( invoice.amount * req.body.discount ) / 100 ).toFixed( 2 );
2367
- invoice.amount = invoice.amount - invoice.discountAmount;
2379
+ invoice.discountAmount = roundAmount( ( invoice.amount * req.body.discount ) / 100, invoice.currency );
2380
+ invoice.amount = roundAmount( invoice.amount - invoice.discountAmount, invoice.currency );
2368
2381
  invoice.discountPercentage = req.body.discount;
2369
2382
  if ( invoice.currency === 'inr' ) {
2370
2383
  if ( invoice.tax.length ) {
2371
2384
  for ( let i = 0; i < invoice.tax.length; i++ ) {
2372
- invoice.tax[i].taxAmount = ( ( invoice.amount * invoice.tax[i].value ) / 100 ).toFixed( 2 );
2373
- invoice.totalAmount = ( Number( invoice.amount ) + Number( invoice.tax[i].taxAmount ) ).toFixed( 2 );
2385
+ invoice.tax[i].taxAmount = String( roundAmount( ( invoice.amount * invoice.tax[i].value ) / 100, invoice.currency ) );
2386
+ invoice.totalAmount = roundAmount( Number( invoice.amount ) + Number( invoice.tax[i].taxAmount ), invoice.currency );
2374
2387
  }
2375
2388
  }
2376
2389
  }
@@ -2500,7 +2513,7 @@ export async function recordPayment( req, res ) {
2500
2513
  }
2501
2514
 
2502
2515
  const previousPaid = Number( invoice.paidAmount ) || 0;
2503
- const newPaid = Math.round( ( previousPaid + amountNum ) * 100 ) / 100;
2516
+ const newPaid = roundAmount( previousPaid + amountNum, invoice.currency );
2504
2517
  const totalAmount = Number( invoice.totalAmount ) || 0;
2505
2518
 
2506
2519
  // Reject overpayment — finance teams want this caught early. They can
@@ -2508,7 +2521,7 @@ export async function recordPayment( req, res ) {
2508
2521
  // invoice total; anything beyond that is a data-entry error.
2509
2522
  if ( totalAmount > 0 && newPaid > totalAmount + 0.01 ) {
2510
2523
  return res.sendError(
2511
- `Payment exceeds outstanding balance. Outstanding: ${( totalAmount - previousPaid ).toFixed( 2 )}`,
2524
+ `Payment exceeds outstanding balance. Outstanding: ${roundAmount( totalAmount - previousPaid, invoice.currency )}`,
2512
2525
  400,
2513
2526
  );
2514
2527
  }
@@ -13,11 +13,13 @@ import * as cameraService from '../services/camera.service.js';
13
13
  import * as billingService from '../services/billing.service.js';
14
14
  import * as paymentAccountService from '../services/paymentAccount.service.js';
15
15
  import * as taggingService from '../services/tagging.service.js';
16
- import { symbolFor } from '../utils/currency.js';
16
+ import { symbolFor, roundAmount, wordFor } from '../utils/currency.js';
17
17
 
18
18
 
19
19
  import dayjs from 'dayjs';
20
- import Handlebars from 'handlebars';
20
+ // Use the shared Handlebars instance that has our custom helpers
21
+ // (ifExists / neq / gtZero) registered, so invoicePdf1.hbs can use them.
22
+ import Handlebars from '../utils/validations/helper/handlebar.helper.js';
21
23
  import fs from 'fs';
22
24
  import path from 'path';
23
25
  // import { JSDOM } from 'jsdom';
@@ -3092,6 +3094,10 @@ export const invoiceDownload = async ( req, res ) => {
3092
3094
  if ( invoiceInfo ) {
3093
3095
  let clientDetails = await paymentService.findOne( { clientId: invoiceInfo.clientId } );
3094
3096
  let amount = 0;
3097
+ // INR shows whole numbers; every other currency keeps 2 decimals.
3098
+ const invoicePdfCurrency = clientDetails?.paymentInvoice?.currencyType;
3099
+ const invoicePdfFractionDigits = invoicePdfCurrency === 'inr' ? 0 : 2;
3100
+ const invoicePdfMoneyFmt = { minimumFractionDigits: invoicePdfFractionDigits, maximumFractionDigits: invoicePdfFractionDigits };
3095
3101
  invoiceInfo.products.forEach( ( item, index ) => {
3096
3102
  let [ firstWord, secondWord ] = item.product.product.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
3097
3103
  firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
@@ -3107,12 +3113,12 @@ export const invoiceDownload = async ( req, res ) => {
3107
3113
 
3108
3114
  amount = amount + item.price;
3109
3115
 
3110
- item.basePrice = Math.round( item.basePrice ).toLocaleString( 'en-IN' );
3111
- item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
3116
+ item.basePrice = roundAmount( item.basePrice, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
3117
+ item.price = roundAmount( item.price, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
3112
3118
  item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
3113
3119
  } );
3114
3120
  for ( let tax of invoiceInfo.tax ) {
3115
- tax.taxAmount = tax.taxAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
3121
+ tax.taxAmount = roundAmount( tax.taxAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
3116
3122
  }
3117
3123
  let invoiceDate = dayjs( invoiceInfo.createdAt ).format( 'DD/MM/YYYY' );
3118
3124
  let days = clientDetails?.paymentInvoice?.extendPaymentPeriodDays || 10;
@@ -3125,7 +3131,7 @@ export const invoiceDownload = async ( req, res ) => {
3125
3131
  clientName: clientDetails.clientName,
3126
3132
  extendDays: clientDetails.paymentInvoice.extendPaymentPeriodDays,
3127
3133
  address: clientDetails.billingDetails.billingAddress,
3128
- subtotal: amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
3134
+ subtotal: roundAmount( amount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
3129
3135
  companyName: invoiceInfo.companyName,
3130
3136
  companyAddress: invoiceInfo.companyAddress,
3131
3137
  PlaceOfSupply: invoiceInfo.PlaceOfSupply,
@@ -3134,7 +3140,8 @@ export const invoiceDownload = async ( req, res ) => {
3134
3140
  amountwords: AmountinWords,
3135
3141
  Terms: `Term ${clientDetails.paymentInvoice.extendPaymentPeriodDays}`,
3136
3142
  currencyType: symbolFor( clientDetails?.paymentInvoice?.currencyType ),
3137
- totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
3143
+ currencyName: wordFor( invoicePdfCurrency ),
3144
+ totalAmount: roundAmount( invoiceInfo.totalAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
3138
3145
  invoiceDate,
3139
3146
  dueDate,
3140
3147
  };
@@ -1401,12 +1401,14 @@
1401
1401
  <div class="chennai-tamil-nadu-600006-india">
1402
1402
 
1403
1403
  </div>
1404
- <div class="gstin-33-aagct-3124-r-1-z-2">GSTIN {{GSTNumber}}</div>
1404
+ {{#ifExists GSTNumber}}<div class="gstin-33-aagct-3124-r-1-z-2">GSTIN {{GSTNumber}}</div>{{/ifExists}}
1405
1405
  </div>
1406
1406
  </div>
1407
+ {{#ifExists PlaceOfSupply}}
1407
1408
  <div class="place_of_supply">
1408
1409
  Place Of Supply: {{PlaceOfSupply}}
1409
1410
  </div>
1411
+ {{/ifExists}}
1410
1412
  </div>
1411
1413
  <div class="frame-2698">
1412
1414
  <div class="frame-9155">
@@ -1560,7 +1562,7 @@
1560
1562
  <div class="frame-23972">
1561
1563
  {{#neq discountAmount 0}}
1562
1564
  <div class="frame-2394">
1563
- <div class="text9">Discount ({{discountPercentage}}%)</div>
1565
+ <div class="text9">Discount</div>
1564
1566
  <div class="frame-9157">
1565
1567
  <div class="text10">{{currencyType}} {{discountAmount}}</div>
1566
1568
  </div>
@@ -1575,12 +1577,14 @@
1575
1577
 
1576
1578
  {{#if gstApplicable}}
1577
1579
  {{#each tax }}
1580
+ {{#gtZero taxAmount}}
1578
1581
  <div class="frame-2392">
1579
1582
  <div class="text9">{{type}} ({{value}}%)</div>
1580
1583
  <div class="frame-9157">
1581
1584
  <div class="text10">{{../currencyType}} {{taxAmount}}</div>
1582
1585
  </div>
1583
1586
  </div>
1587
+ {{/gtZero}}
1584
1588
  {{/each }}
1585
1589
  {{/if}}
1586
1590
 
@@ -1605,22 +1609,7 @@
1605
1609
  <div class="text11">Total In Words</div>
1606
1610
  <div class="frame-9157">
1607
1611
  <div class="text13">
1608
- {{#eq billingCurrency 'inr'}}
1609
- Indian Rupee
1610
- {{/eq}}
1611
-
1612
- {{#eq billingCurrency 'dollar'}}
1613
- US Dollar
1614
- {{/eq}}
1615
-
1616
- {{#eq billingCurrency 'singaporedollar'}}
1617
- Singapore Dollar
1618
- {{/eq}}
1619
-
1620
- {{#eq billingCurrency 'euro'}}
1621
- Euro
1622
- {{/eq}}
1623
- {{amountwords}} Only
1612
+ {{currencyWord}} {{amountwords}} Only
1624
1613
  </div>
1625
1614
  </div>
1626
1615
  </div>
@@ -1368,16 +1368,20 @@
1368
1368
  class="billing-address-given-by-the-client-flat-no-012-ground-floor-the-banyan-apartment-jsr-layout-j-p-nagar-9th-phase-alahalli-anjanapura-post-bengaluru-560062-karnataka-india">
1369
1369
  {{companyAddress}}
1370
1370
  </div>
1371
+ {{#ifExists GSTNumber}}
1371
1372
  <div class="frame-562">
1372
1373
  <div class="gstin-33-aafci-2595-g-1-z-9"> GSTIN {{GSTNumber}}</div>
1373
1374
  </div>
1375
+ {{/ifExists}}
1374
1376
  </div>
1375
1377
  </div>
1376
1378
  </div>
1377
1379
  </div>
1380
+ {{#ifExists PlaceOfSupply}}
1378
1381
  <div class="place-of-supply-tamil-nadu-33">
1379
1382
  Place Of Supply: {{PlaceOfSupply}}
1380
1383
  </div>
1384
+ {{/ifExists}}
1381
1385
  </div>
1382
1386
  <div class="invoice-info">
1383
1387
  <div class="card-header">
@@ -1496,12 +1500,14 @@
1496
1500
  </div>
1497
1501
  </div>
1498
1502
  {{#each tax }}
1503
+ {{#gtZero taxAmount}}
1499
1504
  <div class="frame-2392">
1500
1505
  <div class="text9">{{type}} ({{value}}%)</div>
1501
1506
  <div class="frame-9157">
1502
1507
  <div class="text10">{{currency}} {{taxAmount}}</div>
1503
1508
  </div>
1504
1509
  </div>
1510
+ {{/gtZero}}
1505
1511
  {{/each }}
1506
1512
  <!-- <div class="frame-2398">
1507
1513
  <div class="text9">CGST (9%)</div>
@@ -13,3 +13,36 @@ export const CURRENCY_SYMBOLS = {
13
13
  };
14
14
 
15
15
  export const symbolFor = ( currency ) => CURRENCY_SYMBOLS[currency] ?? '$';
16
+
17
+ /**
18
+ * Words form of each currency, used in the invoice "Total In Words" line.
19
+ * Keys match billing.currency enums. Unknown keys fall back to 'US Dollar'
20
+ * to match the pre-existing default.
21
+ */
22
+ export const CURRENCY_WORDS = {
23
+ inr: 'Indian Rupee',
24
+ dollar: 'US Dollar',
25
+ singaporedollar: 'Singapore Dollar',
26
+ euro: 'Euro',
27
+ aed: 'UAE Dirham',
28
+ };
29
+
30
+ export const wordFor = ( currency ) => CURRENCY_WORDS[currency] ?? 'US Dollar';
31
+
32
+ /**
33
+ * Currency-aware monetary rounding.
34
+ * - INR -> rounded to a whole number (no decimals).
35
+ * - every other currency -> kept to exactly 2 decimal places.
36
+ * Returns a Number (not a string) so downstream arithmetic still works.
37
+ * Non-finite input is normalised to 0.
38
+ *
39
+ * @param {number|string} value - the monetary value to round.
40
+ * @param {string} currency - the currency code (e.g. 'inr', 'dollar').
41
+ * @return {number} the rounded amount.
42
+ */
43
+ export const roundAmount = ( value, currency ) => {
44
+ const n = Number( value );
45
+ if ( !isFinite( n ) ) return 0;
46
+ if ( currency === 'inr' ) return Math.round( n );
47
+ return Math.round( n * 100 ) / 100;
48
+ };
@@ -13,5 +13,11 @@ Handlebars.registerHelper( 'gte', function( a, b, options ) {
13
13
  Handlebars.registerHelper( 'ifExists', function( value, options ) {
14
14
  return value ? options.fn( this ) : options.inverse( this );
15
15
  } );
16
+ // Renders the block only when `value` parses to a number greater than zero.
17
+ // Used to hide tax lines (IGST/CGST/SGST) whose amount is 0 / "0" / "0.00".
18
+ Handlebars.registerHelper( 'gtZero', function( value, options ) {
19
+ const n = Number( String( value ).replace( /,/g, '' ) );
20
+ return isFinite( n ) && n > 0 ? options.fn( this ) : options.inverse( this );
21
+ } );
16
22
 
17
23
  export default Handlebars;