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 +1 -1
- package/src/controllers/invoice.controller.js +59 -46
- package/src/controllers/paymentSubscription.controllers.js +14 -7
- package/src/hbs/invoicePdf.hbs +7 -18
- package/src/hbs/invoicePdf1.hbs +6 -0
- package/src/utils/currency.js +33 -0
- package/src/utils/validations/helper/handlebar.helper.js +6 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
( Array.isArray( req.body.tax ) ? req.body.tax.reduce( ( s, t ) => s + ( Number( t.taxAmount ) || 0 ) * customAdvanceMonths, 0 ) : 0 ) ) :
|
|
171
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
755
|
-
|
|
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 =
|
|
774
|
-
const
|
|
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',
|
|
795
|
-
item.amount = item.amount.toLocaleString( 'en-IN',
|
|
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 =
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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 =
|
|
994
|
-
item.amount = item.amount.toLocaleString( 'en-IN',
|
|
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 =
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1917
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
2233
|
-
|
|
2234
|
-
cards.
|
|
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
|
|
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
|
|
2373
|
-
invoice.totalAmount = ( Number( invoice.amount ) + Number( invoice.tax[i].taxAmount )
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3111
|
-
item.price =
|
|
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',
|
|
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',
|
|
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
|
-
|
|
3143
|
+
currencyName: wordFor( invoicePdfCurrency ),
|
|
3144
|
+
totalAmount: roundAmount( invoiceInfo.totalAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
|
|
3138
3145
|
invoiceDate,
|
|
3139
3146
|
dueDate,
|
|
3140
3147
|
};
|
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -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
|
|
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
|
-
{{
|
|
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>
|
package/src/hbs/invoicePdf1.hbs
CHANGED
|
@@ -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>
|
package/src/utils/currency.js
CHANGED
|
@@ -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;
|