tango-app-api-payment-subscription 3.5.19 → 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 +2 -2
- package/src/controllers/brandsBilling.controller.js +10 -4
- package/src/controllers/invoice.controller.js +75 -52
- package/src/controllers/paymentSubscription.controllers.js +43 -14
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-payment-subscription",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.21",
|
|
4
4
|
"description": "paymentSubscription",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"nodemon": "^3.1.0",
|
|
30
30
|
"puppeteer": "^24.41.0",
|
|
31
31
|
"swagger-ui-express": "^5.0.0",
|
|
32
|
-
"tango-api-schema": "^2.6.
|
|
32
|
+
"tango-api-schema": "^2.6.37",
|
|
33
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
34
34
|
"winston": "^3.12.0",
|
|
35
35
|
"winston-daily-rotate-file": "^5.0.0",
|
|
@@ -579,14 +579,20 @@ export async function brandInvoiceList( req, res ) {
|
|
|
579
579
|
return res.sendError( 'No data', 204 );
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
// Footer totals are shown in a single ₹ figure, but individual invoices may
|
|
583
|
+
// be in USD ($). Convert dollar invoices to INR at today's rate before
|
|
584
|
+
// summing so the ₹ total is correct (mirrors the table's per-invoice symbol
|
|
585
|
+
// → one combined rupee total).
|
|
586
|
+
const usdRate = await getUsdInrRate();
|
|
587
|
+
const toInr = ( inv, value ) => ( inv.currency === 'dollar' ? ( Number( value ) || 0 ) * usdRate : ( Number( value ) || 0 ) );
|
|
582
588
|
let summary = {
|
|
583
589
|
totalInvoices: allInvoices.length,
|
|
584
|
-
totalInvoiced: allInvoices.reduce( ( sum, inv ) => sum + ( inv.totalAmount
|
|
590
|
+
totalInvoiced: Math.round( allInvoices.reduce( ( sum, inv ) => sum + toInr( inv, inv.totalAmount ), 0 ) ),
|
|
585
591
|
// Footer totals over the FULL filtered set (not just the current page):
|
|
586
|
-
// stores, amount excl. GST and amount incl. GST.
|
|
592
|
+
// stores, amount excl. GST and amount incl. GST (USD → INR converted).
|
|
587
593
|
totalStores: allInvoices.reduce( ( sum, inv ) => sum + ( inv.stores || 0 ), 0 ),
|
|
588
|
-
totalAmountExclGst: allInvoices.reduce( ( sum, inv ) => sum + ( inv.amount
|
|
589
|
-
totalAmountInclGst: allInvoices.reduce( ( sum, inv ) => sum + ( inv.totalAmount
|
|
594
|
+
totalAmountExclGst: Math.round( allInvoices.reduce( ( sum, inv ) => sum + toInr( inv, inv.amount ), 0 ) ),
|
|
595
|
+
totalAmountInclGst: Math.round( allInvoices.reduce( ( sum, inv ) => sum + toInr( inv, inv.totalAmount ), 0 ) ),
|
|
590
596
|
pendingApproval: allInvoices.filter( ( inv ) => [ 'pendingCsm', 'pendingFinance', 'pendingApproval' ].includes( inv.status ) ).length,
|
|
591
597
|
pendingPayment: allInvoices.filter( ( inv ) => inv.paymentStatus === 'unpaid' && inv.status === 'approved' ).length,
|
|
592
598
|
paid: allInvoices.filter( ( inv ) => inv.paymentStatus === 'paid' ).length,
|
|
@@ -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}`,
|
|
@@ -287,11 +288,16 @@ export async function createInvoice( req, res ) {
|
|
|
287
288
|
let baseDate = isAdvance ? dayjs().add( 1, 'month' ).startOf( 'month' ) : dayjs();
|
|
288
289
|
let products;
|
|
289
290
|
|
|
291
|
+
// Effective price type: when group-wise pricing applies, the GROUP's own
|
|
292
|
+
// price type (from its basepricing doc) wins; otherwise the client's.
|
|
293
|
+
const { priceType: groupPriceType } = await resolveBasePricingScope( group, getClient );
|
|
294
|
+
const effectivePriceType = groupPriceType || getClient?.priceType;
|
|
295
|
+
|
|
290
296
|
if ( advanceCoverProducts ) {
|
|
291
297
|
// Reuse the advance invoice's current-month line items (store counts +
|
|
292
298
|
// prices already set) and bill them as a normal monthly invoice.
|
|
293
299
|
products = advanceCoverProducts;
|
|
294
|
-
} else if (
|
|
300
|
+
} else if ( effectivePriceType === 'standard' ) {
|
|
295
301
|
products = await standardPrice( group, getClient, baseDate );
|
|
296
302
|
} else {
|
|
297
303
|
products = await stepPrice( group, getClient );
|
|
@@ -391,7 +397,7 @@ export async function createInvoice( req, res ) {
|
|
|
391
397
|
productName: 'oneTimeFee',
|
|
392
398
|
period: 'fullmonth',
|
|
393
399
|
storeCount: newStoreCount,
|
|
394
|
-
amount:
|
|
400
|
+
amount: roundAmount( newStoreCount * oneTimeFeePerStore, group.currency ),
|
|
395
401
|
price: oneTimeFeePerStore,
|
|
396
402
|
description: `One-Time Fee - ${newStoreCount} stores`,
|
|
397
403
|
HsnNumber: '998314',
|
|
@@ -412,21 +418,21 @@ export async function createInvoice( req, res ) {
|
|
|
412
418
|
// existing GST/IGST/CGST/SGST behavior for legacy records that have
|
|
413
419
|
// no taxCalculationType set yet.
|
|
414
420
|
if ( group.taxCalculationType === 'international' ) {
|
|
415
|
-
totalAmount =
|
|
421
|
+
totalAmount = roundAmount( amount, group.currency );
|
|
416
422
|
} else if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
|
|
417
423
|
let taxAmount = ( amount * 18 ) / 100;
|
|
418
|
-
totalAmount =
|
|
424
|
+
totalAmount = roundAmount( amount + taxAmount, group.currency );
|
|
419
425
|
taxList.push(
|
|
420
426
|
{
|
|
421
427
|
'currency': '₹',
|
|
422
428
|
'type': 'CGST',
|
|
423
429
|
'value': 9,
|
|
424
|
-
'taxAmount': ( ( amount * 9 ) / 100
|
|
430
|
+
'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
|
|
425
431
|
}, {
|
|
426
432
|
'currency': '₹',
|
|
427
433
|
'type': 'SGST',
|
|
428
434
|
'value': 9,
|
|
429
|
-
'taxAmount': ( ( amount * 9 ) / 100
|
|
435
|
+
'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
|
|
430
436
|
},
|
|
431
437
|
);
|
|
432
438
|
} else {
|
|
@@ -434,13 +440,13 @@ export async function createInvoice( req, res ) {
|
|
|
434
440
|
if ( group.currency === 'inr' ) {
|
|
435
441
|
taxAmount = ( amount * 18 ) / 100;
|
|
436
442
|
}
|
|
437
|
-
totalAmount =
|
|
443
|
+
totalAmount = roundAmount( amount + taxAmount, group.currency );
|
|
438
444
|
taxList.push(
|
|
439
445
|
{
|
|
440
446
|
'currency': '₹',
|
|
441
447
|
'type': 'IGST',
|
|
442
448
|
'value': 18,
|
|
443
|
-
'taxAmount': ( taxAmount
|
|
449
|
+
'taxAmount': String( roundAmount( taxAmount, group.currency ) ),
|
|
444
450
|
},
|
|
445
451
|
);
|
|
446
452
|
}
|
|
@@ -523,14 +529,14 @@ export async function createInvoice( req, res ) {
|
|
|
523
529
|
invoice: req.body.invoiceId ? req.body.invoiceId : `${invPrefix}${Finacialyear}-${invoiceNo}`,
|
|
524
530
|
products: products,
|
|
525
531
|
status: 'pendingCsm',
|
|
526
|
-
amount:
|
|
532
|
+
amount: roundAmount( amount, group.currency ),
|
|
527
533
|
invoiceIndex: req.body.invoiceId ? findInvoice.invoiceIndex : invoiceNo,
|
|
528
534
|
tax: taxList,
|
|
529
535
|
companyName: ( group.registeredCompanyName || '' ).toUpperCase(),
|
|
530
536
|
companyAddress: address,
|
|
531
537
|
PlaceOfSupply: group.placeOfSupply,
|
|
532
538
|
GSTNumber: group.gst,
|
|
533
|
-
totalAmount:
|
|
539
|
+
totalAmount: roundAmount( totalAmount, group.currency ),
|
|
534
540
|
clientId: group.clientId,
|
|
535
541
|
paymentMethod: 'Online',
|
|
536
542
|
billingDate: new Date( invoicedate ),
|
|
@@ -746,8 +752,8 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
|
746
752
|
// store's position within the product.
|
|
747
753
|
const price = priceForStore( s.productName, productPosition[s.productName] );
|
|
748
754
|
const runningCost = s.workingdays >= monthDays ?
|
|
749
|
-
|
|
750
|
-
|
|
755
|
+
roundAmount( price * units, invoiceInfo.currency ) :
|
|
756
|
+
roundAmount( ( price / monthDays ) * s.workingdays * units, invoiceInfo.currency );
|
|
751
757
|
return {
|
|
752
758
|
productName: s.productName ? s.productName.charAt( 0 ).toUpperCase() + s.productName.slice( 1 ) : '',
|
|
753
759
|
currencyType: invoiceCurrency,
|
|
@@ -765,8 +771,9 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
|
765
771
|
};
|
|
766
772
|
} );
|
|
767
773
|
|
|
768
|
-
const totalAmount =
|
|
769
|
-
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 } );
|
|
770
777
|
return { data, totalAmount, totalFormatted };
|
|
771
778
|
}
|
|
772
779
|
|
|
@@ -781,20 +788,23 @@ export async function invoiceDownload( req, res ) {
|
|
|
781
788
|
// client.paymentInvoice or virtualAccount.currency causes historical
|
|
782
789
|
// invoices to re-render in the wrong currency if those settings change.
|
|
783
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 };
|
|
784
794
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
785
795
|
item.index = index + 1;
|
|
786
796
|
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
787
797
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
788
798
|
item.productName = firstWord + ' ' + secondWord;
|
|
789
|
-
item.price = item.price .toLocaleString( 'en-IN',
|
|
790
|
-
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 );
|
|
791
801
|
item.currency = invoiceCurrency;
|
|
792
802
|
} );
|
|
793
803
|
|
|
794
804
|
|
|
795
805
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
796
806
|
|
|
797
|
-
invoiceInfo.totalAmount =
|
|
807
|
+
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
798
808
|
let AmountinWords = inWords( invoiceInfo.totalAmount );
|
|
799
809
|
let getgroup;
|
|
800
810
|
let days = getgroup?.paymentTerm ? getgroup?.paymentTerm : '30';
|
|
@@ -809,10 +819,10 @@ export async function invoiceDownload( req, res ) {
|
|
|
809
819
|
invoiceData = {
|
|
810
820
|
...invoiceInfo._doc,
|
|
811
821
|
clientName: clientDetails.clientName,
|
|
812
|
-
amount: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
822
|
+
amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
813
823
|
extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
|
|
814
824
|
address: clientDetails.billingDetails.billingAddress,
|
|
815
|
-
subtotal: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
825
|
+
subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
816
826
|
companyName: invoiceInfo.companyName,
|
|
817
827
|
companyAddress: invoiceInfo.companyAddress,
|
|
818
828
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -821,7 +831,8 @@ export async function invoiceDownload( req, res ) {
|
|
|
821
831
|
amountwords: AmountinWords,
|
|
822
832
|
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
823
833
|
currencyType: invoiceCurrency,
|
|
824
|
-
|
|
834
|
+
currencyWord: wordFor( invoiceInfo.currency ),
|
|
835
|
+
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
|
|
825
836
|
invoiceDate,
|
|
826
837
|
dueDate,
|
|
827
838
|
discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
|
|
@@ -980,18 +991,21 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
980
991
|
// field, recorded at creation from the billing group. See invoiceDownload
|
|
981
992
|
// above for the same pattern.
|
|
982
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 };
|
|
983
997
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
984
998
|
item.index = index + 1;
|
|
985
999
|
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
986
1000
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
987
1001
|
item.productName = firstWord + ' ' + secondWord;
|
|
988
|
-
item.price =
|
|
989
|
-
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 );
|
|
990
1004
|
item.currency = invoiceCurrency;
|
|
991
1005
|
} );
|
|
992
1006
|
|
|
993
1007
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
994
|
-
invoiceInfo.totalAmount =
|
|
1008
|
+
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
995
1009
|
let AmountinWords = inWords( invoiceInfo.totalAmount );
|
|
996
1010
|
let getgroup;
|
|
997
1011
|
if ( invoiceInfo.groupId ) {
|
|
@@ -1005,10 +1019,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
1005
1019
|
let invoiceData = {
|
|
1006
1020
|
...invoiceInfo._doc,
|
|
1007
1021
|
clientName: clientDetails.clientName,
|
|
1008
|
-
amount: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
1022
|
+
amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
1009
1023
|
extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
|
|
1010
1024
|
address: clientDetails.billingDetails.billingAddress,
|
|
1011
|
-
subtotal: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
1025
|
+
subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
1012
1026
|
companyName: invoiceInfo.companyName,
|
|
1013
1027
|
companyAddress: invoiceInfo.companyAddress,
|
|
1014
1028
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -1017,7 +1031,8 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
1017
1031
|
amountwords: AmountinWords,
|
|
1018
1032
|
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
1019
1033
|
currencyType: invoiceCurrency,
|
|
1020
|
-
|
|
1034
|
+
currencyWord: wordFor( invoiceInfo.currency ),
|
|
1035
|
+
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
|
|
1021
1036
|
invoiceDate,
|
|
1022
1037
|
dueDate,
|
|
1023
1038
|
discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
|
|
@@ -1207,20 +1222,25 @@ function inWords( num ) {
|
|
|
1207
1222
|
// Resolve which basepricing doc applies to a billing group. When the client
|
|
1208
1223
|
// has billingGroupWisePricing enabled AND a doc exists for this group, returns
|
|
1209
1224
|
// that group's doc; otherwise falls back to the brand-level doc (groupId unset).
|
|
1210
|
-
// Returns { query, groupId } where query is the mongo filter for the
|
|
1211
|
-
//
|
|
1225
|
+
// Returns { query, groupId, priceType } where query is the mongo filter for the
|
|
1226
|
+
// chosen doc, groupId is the id for aggregation lookups (null for brand), and
|
|
1227
|
+
// priceType is the group's own price type when group-wise (else null).
|
|
1212
1228
|
async function resolveBasePricingScope( group, getClient ) {
|
|
1213
1229
|
const brandQuery = { clientId: group.clientId, groupId: { $exists: false } };
|
|
1214
1230
|
if ( getClient?.billingGroupWisePricing && group?._id ) {
|
|
1215
1231
|
const groupIdStr = String( group._id );
|
|
1216
1232
|
const groupDoc = await basepricingService.findOne(
|
|
1217
|
-
{ clientId: group.clientId, groupId: groupIdStr }, { _id: 1 },
|
|
1233
|
+
{ clientId: group.clientId, groupId: groupIdStr }, { _id: 1, priceType: 1 },
|
|
1218
1234
|
);
|
|
1219
1235
|
if ( groupDoc ) {
|
|
1220
|
-
return {
|
|
1236
|
+
return {
|
|
1237
|
+
query: { clientId: group.clientId, groupId: groupIdStr },
|
|
1238
|
+
groupId: groupIdStr,
|
|
1239
|
+
priceType: groupDoc.priceType || null,
|
|
1240
|
+
};
|
|
1221
1241
|
}
|
|
1222
1242
|
}
|
|
1223
|
-
return { query: brandQuery, groupId: null };
|
|
1243
|
+
return { query: brandQuery, groupId: null, priceType: null };
|
|
1224
1244
|
}
|
|
1225
1245
|
|
|
1226
1246
|
async function standardPrice( group, getClient, baseDate ) {
|
|
@@ -1628,7 +1648,7 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
1628
1648
|
if ( store.workingDays >= currentMonthDays ) {
|
|
1629
1649
|
amount = store.price * storeCount;
|
|
1630
1650
|
} else {
|
|
1631
|
-
amount =
|
|
1651
|
+
amount = roundAmount( ( store.price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
|
|
1632
1652
|
}
|
|
1633
1653
|
|
|
1634
1654
|
let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
|
|
@@ -1827,7 +1847,7 @@ async function stepPrice( group, getClient ) {
|
|
|
1827
1847
|
if ( store.workingDays >= currentMonthDays ) {
|
|
1828
1848
|
amount = price * storeCount;
|
|
1829
1849
|
} else {
|
|
1830
|
-
amount =
|
|
1850
|
+
amount = roundAmount( ( price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
|
|
1831
1851
|
}
|
|
1832
1852
|
|
|
1833
1853
|
let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
|
|
@@ -1903,8 +1923,8 @@ async function stepPrice( group, getClient ) {
|
|
|
1903
1923
|
const price = tierPriceForPosition( s.productName, productPosition[s.productName] );
|
|
1904
1924
|
const fullMonth = s.workingDays >= currentMonthDays;
|
|
1905
1925
|
const amount = fullMonth ?
|
|
1906
|
-
|
|
1907
|
-
|
|
1926
|
+
roundAmount( units * price, group.currency ) :
|
|
1927
|
+
roundAmount( units * ( price / currentMonthDays ) * s.workingDays, group.currency );
|
|
1908
1928
|
const period = fullMonth ? 'fullMonth' : 'proRate';
|
|
1909
1929
|
const key = `${s.productName}_${period}_${price}`;
|
|
1910
1930
|
if ( !grouped[key] ) {
|
|
@@ -1921,7 +1941,8 @@ async function stepPrice( group, getClient ) {
|
|
|
1921
1941
|
} else if ( grp.productName === 'tangoZone' ) {
|
|
1922
1942
|
description = 'Product category/section analytics';
|
|
1923
1943
|
}
|
|
1924
|
-
const amount =
|
|
1944
|
+
const amount = roundAmount( grp.totalAmount, group.currency );
|
|
1945
|
+
const zeroPrice = group.currency === 'inr' ? '0' : '0.00';
|
|
1925
1946
|
return {
|
|
1926
1947
|
productName: grp.productName,
|
|
1927
1948
|
period: grp.period,
|
|
@@ -1933,7 +1954,7 @@ async function stepPrice( group, getClient ) {
|
|
|
1933
1954
|
HsnNumber: '998314',
|
|
1934
1955
|
amount: amount,
|
|
1935
1956
|
month: dayjs().format( 'MMM YYYY' ),
|
|
1936
|
-
price: grp.unitCount ? ( amount / grp.unitCount
|
|
1957
|
+
price: grp.unitCount ? String( roundAmount( amount / grp.unitCount, group.currency ) ) : zeroPrice,
|
|
1937
1958
|
};
|
|
1938
1959
|
} );
|
|
1939
1960
|
|
|
@@ -2219,9 +2240,11 @@ export async function clientInvoiceList( req, res ) {
|
|
|
2219
2240
|
cards.pendingPaymentCount += 1;
|
|
2220
2241
|
}
|
|
2221
2242
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
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' );
|
|
2225
2248
|
|
|
2226
2249
|
res.sendSuccess( { count: count.length, data: invoiceList, cards } );
|
|
2227
2250
|
} catch ( error ) {
|
|
@@ -2353,14 +2376,14 @@ export async function applyDiscount( req, res ) {
|
|
|
2353
2376
|
try {
|
|
2354
2377
|
let invoice = await invoiceService.findOne( { invoice: req.body.invoice } );
|
|
2355
2378
|
if ( invoice ) {
|
|
2356
|
-
invoice.discountAmount = ( ( invoice.amount * req.body.discount ) / 100
|
|
2357
|
-
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 );
|
|
2358
2381
|
invoice.discountPercentage = req.body.discount;
|
|
2359
2382
|
if ( invoice.currency === 'inr' ) {
|
|
2360
2383
|
if ( invoice.tax.length ) {
|
|
2361
2384
|
for ( let i = 0; i < invoice.tax.length; i++ ) {
|
|
2362
|
-
invoice.tax[i].taxAmount = ( ( invoice.amount * invoice.tax[i].value ) / 100
|
|
2363
|
-
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 );
|
|
2364
2387
|
}
|
|
2365
2388
|
}
|
|
2366
2389
|
}
|
|
@@ -2490,7 +2513,7 @@ export async function recordPayment( req, res ) {
|
|
|
2490
2513
|
}
|
|
2491
2514
|
|
|
2492
2515
|
const previousPaid = Number( invoice.paidAmount ) || 0;
|
|
2493
|
-
const newPaid =
|
|
2516
|
+
const newPaid = roundAmount( previousPaid + amountNum, invoice.currency );
|
|
2494
2517
|
const totalAmount = Number( invoice.totalAmount ) || 0;
|
|
2495
2518
|
|
|
2496
2519
|
// Reject overpayment — finance teams want this caught early. They can
|
|
@@ -2498,7 +2521,7 @@ export async function recordPayment( req, res ) {
|
|
|
2498
2521
|
// invoice total; anything beyond that is a data-entry error.
|
|
2499
2522
|
if ( totalAmount > 0 && newPaid > totalAmount + 0.01 ) {
|
|
2500
2523
|
return res.sendError(
|
|
2501
|
-
`Payment exceeds outstanding balance. Outstanding: ${( totalAmount - previousPaid
|
|
2524
|
+
`Payment exceeds outstanding balance. Outstanding: ${roundAmount( totalAmount - previousPaid, invoice.currency )}`,
|
|
2502
2525
|
400,
|
|
2503
2526
|
);
|
|
2504
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';
|
|
@@ -2256,12 +2258,15 @@ export const priceList = async ( req, res ) => {
|
|
|
2256
2258
|
} else {
|
|
2257
2259
|
priceQuery.groupId = { $exists: false };
|
|
2258
2260
|
}
|
|
2259
|
-
let pricingDetails = await basePricingService.findOne( priceQuery, { standard: 1, step: 1, oneTimeFeePerStore: 1 } );
|
|
2261
|
+
let pricingDetails = await basePricingService.findOne( priceQuery, { standard: 1, step: 1, oneTimeFeePerStore: 1, priceType: 1 } );
|
|
2260
2262
|
if ( !pricingDetails ) {
|
|
2261
2263
|
return res.sendError( 'no data found', 204 );
|
|
2262
2264
|
}
|
|
2265
|
+
// The price type for THIS doc: the doc's own priceType wins (group-wise
|
|
2266
|
+
// pricing stores it per group); fall back to the requested priceType.
|
|
2267
|
+
const effectivePriceType = pricingDetails.priceType || req.body.priceType;
|
|
2263
2268
|
let data = [];
|
|
2264
|
-
if (
|
|
2269
|
+
if ( effectivePriceType == 'standard' ) {
|
|
2265
2270
|
data = pricingDetails.standard;
|
|
2266
2271
|
} else {
|
|
2267
2272
|
data = pricingDetails.step;
|
|
@@ -2301,7 +2306,7 @@ export const priceList = async ( req, res ) => {
|
|
|
2301
2306
|
if ( !product.billingType ) {
|
|
2302
2307
|
product.billingType = 'perStore';
|
|
2303
2308
|
}
|
|
2304
|
-
if (
|
|
2309
|
+
if ( effectivePriceType == 'standard' ) {
|
|
2305
2310
|
product.storeCount = item.storeCount;
|
|
2306
2311
|
} else {
|
|
2307
2312
|
product.showImg = false;
|
|
@@ -2351,6 +2356,8 @@ export const priceList = async ( req, res ) => {
|
|
|
2351
2356
|
let finalValue = parseFloat( discountTotalPrice ) + gstAmount;
|
|
2352
2357
|
let result = {
|
|
2353
2358
|
product: data,
|
|
2359
|
+
// The price type actually used for this doc (group-wise type wins).
|
|
2360
|
+
priceType: effectivePriceType,
|
|
2354
2361
|
oneTimeFeePerStore: pricingDetails.oneTimeFeePerStore != null ? pricingDetails.oneTimeFeePerStore : null,
|
|
2355
2362
|
totalActualPrice: originalTotalPrice,
|
|
2356
2363
|
totalNegotiatePrice: discountTotalPrice.toFixed( 2 ),
|
|
@@ -2516,16 +2523,23 @@ export const pricingListUpdate = async ( req, res ) => {
|
|
|
2516
2523
|
if ( req.body.oneTimeFeePerStore != null && req.body.oneTimeFeePerStore !== '' ) {
|
|
2517
2524
|
getPriceInfo.oneTimeFeePerStore = Number( req.body.oneTimeFeePerStore ) || 0;
|
|
2518
2525
|
}
|
|
2519
|
-
// Keep the group identity on the doc when saving group-wise
|
|
2526
|
+
// Keep the group identity + price type on the doc when saving group-wise
|
|
2527
|
+
// pricing, so each group remembers its own standard/step choice.
|
|
2520
2528
|
if ( req.body.groupId ) {
|
|
2521
2529
|
getPriceInfo.groupId = req.body.groupId;
|
|
2522
2530
|
if ( req.body.groupName ) {
|
|
2523
2531
|
getPriceInfo.groupName = req.body.groupName;
|
|
2524
2532
|
}
|
|
2533
|
+
getPriceInfo.priceType = req.body.type;
|
|
2525
2534
|
}
|
|
2526
2535
|
getPriceInfo.save().then( async () => {
|
|
2527
2536
|
let clientDetails = await paymentService.findOne( { clientId: req.body.clientId }, { priceType: 1, paymentInvoice: 1, planDetails: 1 } );
|
|
2528
|
-
|
|
2537
|
+
// For group-wise pricing the client-level priceType must NOT change —
|
|
2538
|
+
// each group keeps its own type on its basepricing doc. Only update the
|
|
2539
|
+
// client's priceType for brand-level (non-group) saves.
|
|
2540
|
+
if ( !req.body.groupId ) {
|
|
2541
|
+
clientDetails.priceType = req.body.type;
|
|
2542
|
+
}
|
|
2529
2543
|
|
|
2530
2544
|
// Update billingType per product in client's planDetails
|
|
2531
2545
|
if ( req.body.products && req.body.products.length && clientDetails.planDetails?.product?.length ) {
|
|
@@ -2690,11 +2704,15 @@ async function updatePricing( req, res, update ) {
|
|
|
2690
2704
|
let product = [];
|
|
2691
2705
|
let clientId = req.body.clientId;
|
|
2692
2706
|
let paymentInvoice = clientDetails.paymentInvoice;
|
|
2693
|
-
|
|
2707
|
+
// Group-wise pricing only creates/updates the GROUP's basepricing doc — it
|
|
2708
|
+
// must NOT re-run the client-level first-time setup (which resets the
|
|
2709
|
+
// client's currencyType, invoice recipients and product trial statuses).
|
|
2710
|
+
// That initialization is only for a brand-level (non-group) first save.
|
|
2711
|
+
if ( !update && !req.body.groupId ) {
|
|
2694
2712
|
let userDetails = await userService.findOne( { clientId: req.body.clientId, role: 'superadmin' } );
|
|
2695
2713
|
paymentInvoice.invoiceTo = [ userDetails.email ];
|
|
2696
2714
|
paymentInvoice.paymentAgreementTo = [ userDetails.email ];
|
|
2697
|
-
paymentInvoice.currencyType = userDetails.countryCode == '91' ? 'inr' : 'dollar';
|
|
2715
|
+
// paymentInvoice.currencyType = userDetails.countryCode == '91' ? 'inr' : 'dollar';
|
|
2698
2716
|
clientDetails.planDetails.product.forEach( ( item ) => {
|
|
2699
2717
|
product.push( {
|
|
2700
2718
|
productName: item.productName,
|
|
@@ -2706,6 +2724,12 @@ async function updatePricing( req, res, update ) {
|
|
|
2706
2724
|
} else {
|
|
2707
2725
|
product = clientDetails.planDetails.product;
|
|
2708
2726
|
}
|
|
2727
|
+
// Group-wise pricing: stop here — the group's basepricing doc was already
|
|
2728
|
+
// created/updated above. Do NOT touch the client's priceType, currency,
|
|
2729
|
+
// products or payment status (that's brand-level state only).
|
|
2730
|
+
if ( req.body.groupId ) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2709
2733
|
req.body = {
|
|
2710
2734
|
'camaraPerSqft': clientDetails.planDetails.storeSize,
|
|
2711
2735
|
'storesCount': clientDetails.planDetails.totalStores,
|
|
@@ -3070,6 +3094,10 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3070
3094
|
if ( invoiceInfo ) {
|
|
3071
3095
|
let clientDetails = await paymentService.findOne( { clientId: invoiceInfo.clientId } );
|
|
3072
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 };
|
|
3073
3101
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
3074
3102
|
let [ firstWord, secondWord ] = item.product.product.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
3075
3103
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
@@ -3085,12 +3113,12 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3085
3113
|
|
|
3086
3114
|
amount = amount + item.price;
|
|
3087
3115
|
|
|
3088
|
-
item.basePrice =
|
|
3089
|
-
item.price =
|
|
3116
|
+
item.basePrice = roundAmount( item.basePrice, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3117
|
+
item.price = roundAmount( item.price, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3090
3118
|
item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
|
|
3091
3119
|
} );
|
|
3092
3120
|
for ( let tax of invoiceInfo.tax ) {
|
|
3093
|
-
tax.taxAmount = tax.taxAmount.toLocaleString( 'en-IN',
|
|
3121
|
+
tax.taxAmount = roundAmount( tax.taxAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3094
3122
|
}
|
|
3095
3123
|
let invoiceDate = dayjs( invoiceInfo.createdAt ).format( 'DD/MM/YYYY' );
|
|
3096
3124
|
let days = clientDetails?.paymentInvoice?.extendPaymentPeriodDays || 10;
|
|
@@ -3103,7 +3131,7 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3103
3131
|
clientName: clientDetails.clientName,
|
|
3104
3132
|
extendDays: clientDetails.paymentInvoice.extendPaymentPeriodDays,
|
|
3105
3133
|
address: clientDetails.billingDetails.billingAddress,
|
|
3106
|
-
subtotal: amount.toLocaleString( 'en-IN',
|
|
3134
|
+
subtotal: roundAmount( amount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
|
|
3107
3135
|
companyName: invoiceInfo.companyName,
|
|
3108
3136
|
companyAddress: invoiceInfo.companyAddress,
|
|
3109
3137
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -3112,7 +3140,8 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3112
3140
|
amountwords: AmountinWords,
|
|
3113
3141
|
Terms: `Term ${clientDetails.paymentInvoice.extendPaymentPeriodDays}`,
|
|
3114
3142
|
currencyType: symbolFor( clientDetails?.paymentInvoice?.currencyType ),
|
|
3115
|
-
|
|
3143
|
+
currencyName: wordFor( invoicePdfCurrency ),
|
|
3144
|
+
totalAmount: roundAmount( invoiceInfo.totalAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
|
|
3116
3145
|
invoiceDate,
|
|
3117
3146
|
dueDate,
|
|
3118
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;
|