tango-app-api-payment-subscription 3.5.21 → 3.5.22
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/brandsBilling.controller.js +12 -3
- package/src/controllers/estimate.controller.js +16 -10
- package/src/controllers/invoice.controller.js +28 -2
- package/src/controllers/paymentSubscription.controllers.js +26 -1
- package/src/hbs/estimatePdf.hbs +0 -1
- package/src/hbs/invoicePdf.hbs +6 -0
- package/src/hbs/invoicePdf1.hbs +6 -0
package/package.json
CHANGED
|
@@ -1944,7 +1944,11 @@ export async function billingSummary( req, res ) {
|
|
|
1944
1944
|
csm: [ ...( csmByClient.get( key ) || [] ) ].join( ', ' ),
|
|
1945
1945
|
revenueMonths: {},
|
|
1946
1946
|
billedStoresMonths: {},
|
|
1947
|
-
|
|
1947
|
+
// Installation fee per month (keyed YYYY-MM). The summary surfaces
|
|
1948
|
+
// ONLY the current month's value — see installationOut below — so the
|
|
1949
|
+
// column stays hidden until the current-month invoice is generated
|
|
1950
|
+
// with an installation fee.
|
|
1951
|
+
installationMonths: {},
|
|
1948
1952
|
invDollar: 0,
|
|
1949
1953
|
invInr: 0,
|
|
1950
1954
|
} );
|
|
@@ -1956,7 +1960,8 @@ export async function billingSummary( req, res ) {
|
|
|
1956
1960
|
const r = rowOf( inv._id.c );
|
|
1957
1961
|
r.revenueMonths[inv._id.ym] = Math.round( ( ( inv.revenueInr || 0 ) + ( inv.revenueUsd || 0 ) * usdRate ) * 100 ) / 100;
|
|
1958
1962
|
r.billedStoresMonths[inv._id.ym] = inv.stores || 0;
|
|
1959
|
-
r.
|
|
1963
|
+
r.installationMonths[inv._id.ym] = ( r.installationMonths[inv._id.ym] || 0 ) +
|
|
1964
|
+
( inv.installationInr || 0 ) + ( inv.installationUsd || 0 ) * usdRate;
|
|
1960
1965
|
r.invDollar += ( inv.dollarInvoices || 0 );
|
|
1961
1966
|
r.invInr += ( inv.inrInvoices || 0 );
|
|
1962
1967
|
if ( inv.companyName ) {
|
|
@@ -2074,7 +2079,11 @@ export async function billingSummary( req, res ) {
|
|
|
2074
2079
|
// in native USD for dollar clients.
|
|
2075
2080
|
revCur = revCur == null ? null : Math.round( revCur );
|
|
2076
2081
|
const revPrevOut = revPrev == null ? null : Math.round( revPrev );
|
|
2077
|
-
|
|
2082
|
+
// Show the installation fee ONLY for the current month, and only once
|
|
2083
|
+
// that month's invoice (which carries the installationFee line) exists.
|
|
2084
|
+
// Until then it's null so the UI hides the whole column.
|
|
2085
|
+
const curInstallation = r.installationMonths[curKey] || 0;
|
|
2086
|
+
const installationOut = curInstallation ? Math.round( curInstallation ) : null;
|
|
2078
2087
|
// Registered Entity from the billings collection (distinct group names).
|
|
2079
2088
|
// First name shown in the column; the full list is sent so the UI can
|
|
2080
2089
|
// reveal the others on hover. Fall back to the invoice company name when
|
|
@@ -7,7 +7,7 @@ import Handlebars from '../utils/validations/helper/handlebar.helper.js';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import htmlpdf from 'html-pdf-node';
|
|
10
|
-
import { symbolFor } from '../utils/currency.js';
|
|
10
|
+
import { symbolFor, roundAmount } from '../utils/currency.js';
|
|
11
11
|
import { getInvoiceCcEmails } from './invoice.controller.js';
|
|
12
12
|
|
|
13
13
|
// Build the estimate PDF buffer + the template data + a safe filename. Shared
|
|
@@ -45,7 +45,9 @@ async function buildEstimatePdf( estimate ) {
|
|
|
45
45
|
companyAddress: e.companyAddress || '',
|
|
46
46
|
GSTNumber: e.GSTNumber || '',
|
|
47
47
|
PlaceOfSupply: e.PlaceOfSupply || '',
|
|
48
|
-
|
|
48
|
+
// "Default Group" is the placeholder used when no real billing group was
|
|
49
|
+
// chosen — don't surface it on the estimate (the template hides empty).
|
|
50
|
+
groupName: ( e.groupName && e.groupName !== 'Default Group' ) ? e.groupName : '',
|
|
49
51
|
period: e.period || '',
|
|
50
52
|
createdDate: e.createdDate ? dayjs( e.createdDate ).format( 'DD/MM/YYYY' ) : '',
|
|
51
53
|
validTill: e.validTill ? dayjs( e.validTill ).format( 'DD/MM/YYYY' ) : '',
|
|
@@ -211,11 +213,13 @@ export async function createEstimate( req, res ) {
|
|
|
211
213
|
{ 'clientName': 1, 'paymentInvoice.currencyType': 1 },
|
|
212
214
|
);
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
// INR rounds to whole; other currencies keep 2 decimals.
|
|
217
|
+
const estimateCurrency = b.currency || ( client?.paymentInvoice?.currencyType === 'dollar' ? 'dollar' : 'inr' );
|
|
218
|
+
const amount = roundAmount( Number( b.amount ) || 0, estimateCurrency );
|
|
219
|
+
let totalAmount = roundAmount( Number( b.totalAmount ) || 0, estimateCurrency );
|
|
216
220
|
if ( !totalAmount && amount ) {
|
|
217
221
|
// Default to 18% GST when caller sends only the pre-tax amount.
|
|
218
|
-
totalAmount =
|
|
222
|
+
totalAmount = roundAmount( amount * 1.18, estimateCurrency );
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
const { estimate, estimateIndex } = await nextEstimateNumber();
|
|
@@ -239,7 +243,7 @@ export async function createEstimate( req, res ) {
|
|
|
239
243
|
tax: Array.isArray( b.tax ) ? b.tax : [],
|
|
240
244
|
amount,
|
|
241
245
|
totalAmount,
|
|
242
|
-
currency:
|
|
246
|
+
currency: estimateCurrency,
|
|
243
247
|
status: b.status === 'sent' ? 'sent' : 'pending',
|
|
244
248
|
createdDate,
|
|
245
249
|
validTill,
|
|
@@ -273,10 +277,12 @@ export async function updateEstimate( req, res ) {
|
|
|
273
277
|
return res.sendError( 'An accepted estimate cannot be edited.', 409 );
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
// INR rounds to whole; other currencies keep 2 decimals.
|
|
281
|
+
const estimateCurrency = b.currency || existing.currency;
|
|
282
|
+
const amount = roundAmount( Number( b.amount ) || 0, estimateCurrency );
|
|
283
|
+
let totalAmount = roundAmount( Number( b.totalAmount ) || 0, estimateCurrency );
|
|
278
284
|
if ( !totalAmount && amount ) {
|
|
279
|
-
totalAmount =
|
|
285
|
+
totalAmount = roundAmount( amount * 1.18, estimateCurrency );
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
// Only the editable fields are updated. The estimate number, index,
|
|
@@ -294,7 +300,7 @@ export async function updateEstimate( req, res ) {
|
|
|
294
300
|
tax: Array.isArray( b.tax ) ? b.tax : existing.tax,
|
|
295
301
|
amount,
|
|
296
302
|
totalAmount,
|
|
297
|
-
currency:
|
|
303
|
+
currency: estimateCurrency,
|
|
298
304
|
notes: b.notes ?? existing.notes,
|
|
299
305
|
updatedBy: req.user?.email || req.user?.userName || '',
|
|
300
306
|
};
|
|
@@ -805,7 +805,7 @@ export async function invoiceDownload( req, res ) {
|
|
|
805
805
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
806
806
|
|
|
807
807
|
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
808
|
-
let AmountinWords =
|
|
808
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
809
809
|
let getgroup;
|
|
810
810
|
let days = getgroup?.paymentTerm ? getgroup?.paymentTerm : '30';
|
|
811
811
|
let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
|
|
@@ -1006,7 +1006,7 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
1006
1006
|
|
|
1007
1007
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
1008
1008
|
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
1009
|
-
let AmountinWords =
|
|
1009
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
1010
1010
|
let getgroup;
|
|
1011
1011
|
if ( invoiceInfo.groupId ) {
|
|
1012
1012
|
getgroup = await billingService.findOne( { _id: invoiceInfo.groupId } );
|
|
@@ -1218,6 +1218,32 @@ function inWords( num ) {
|
|
|
1218
1218
|
return str.toLowerCase().split( ' ' ).map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ).join( ' ' );
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
// Sub-unit (fraction) name per currency for the "Total In Words" cents part.
|
|
1222
|
+
// INR is whole-number only, so it has no fraction.
|
|
1223
|
+
const CURRENCY_FRACTION_WORDS = {
|
|
1224
|
+
dollar: 'Cents',
|
|
1225
|
+
singaporedollar: 'Cents',
|
|
1226
|
+
euro: 'Cents',
|
|
1227
|
+
aed: 'Fils',
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Spell out an amount. INR -> whole number only. Other currencies keep 2
|
|
1231
|
+
// decimals, so the cents are spelled out too when non-zero
|
|
1232
|
+
// (e.g. 3191.10 -> "Three Thousand One Hundred And Ninety One and Ten Cents").
|
|
1233
|
+
function amountToWords( value, currency ) {
|
|
1234
|
+
const n = Number( value ) || 0;
|
|
1235
|
+
if ( currency === 'inr' ) {
|
|
1236
|
+
return inWords( Math.round( n ) );
|
|
1237
|
+
}
|
|
1238
|
+
const whole = Math.trunc( n );
|
|
1239
|
+
const cents = Math.round( ( n - whole ) * 100 );
|
|
1240
|
+
const fraction = CURRENCY_FRACTION_WORDS[currency] || 'Cents';
|
|
1241
|
+
if ( cents > 0 ) {
|
|
1242
|
+
return `${inWords( whole )} and ${inWords( cents )} ${fraction}`;
|
|
1243
|
+
}
|
|
1244
|
+
return inWords( whole );
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1221
1247
|
|
|
1222
1248
|
// Resolve which basepricing doc applies to a billing group. When the client
|
|
1223
1249
|
// has billingGroupWisePricing enabled AND a doc exists for this group, returns
|
|
@@ -3125,7 +3125,7 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3125
3125
|
let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
|
|
3126
3126
|
|
|
3127
3127
|
invoiceInfo.totalAmount = invoiceInfo.totalAmount;
|
|
3128
|
-
let AmountinWords =
|
|
3128
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoicePdfCurrency );
|
|
3129
3129
|
invoiceData = {
|
|
3130
3130
|
...invoiceInfo._doc,
|
|
3131
3131
|
clientName: clientDetails.clientName,
|
|
@@ -3254,6 +3254,31 @@ function inWords( num ) {
|
|
|
3254
3254
|
return str.toLowerCase().split( ' ' ).map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ).join( ' ' );
|
|
3255
3255
|
}
|
|
3256
3256
|
|
|
3257
|
+
// Sub-unit (fraction) name per currency for the "Total In Words" cents part.
|
|
3258
|
+
const CURRENCY_FRACTION_WORDS = {
|
|
3259
|
+
dollar: 'Cents',
|
|
3260
|
+
singaporedollar: 'Cents',
|
|
3261
|
+
euro: 'Cents',
|
|
3262
|
+
aed: 'Fils',
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
// Spell out an amount. INR -> whole number only. Other currencies keep 2
|
|
3266
|
+
// decimals, so the cents are spelled out too when non-zero
|
|
3267
|
+
// (e.g. 383.04 -> "Three Hundred And Eighty Three and Four Cents").
|
|
3268
|
+
function amountToWords( value, currency ) {
|
|
3269
|
+
const n = Number( value ) || 0;
|
|
3270
|
+
if ( currency === 'inr' ) {
|
|
3271
|
+
return inWords( Math.round( n ) );
|
|
3272
|
+
}
|
|
3273
|
+
const whole = Math.trunc( n );
|
|
3274
|
+
const cents = Math.round( ( n - whole ) * 100 );
|
|
3275
|
+
const fraction = CURRENCY_FRACTION_WORDS[currency] || 'Cents';
|
|
3276
|
+
if ( cents > 0 ) {
|
|
3277
|
+
return `${inWords( whole )} and ${inWords( cents )} ${fraction}`;
|
|
3278
|
+
}
|
|
3279
|
+
return inWords( whole );
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3257
3282
|
export const updateInvoiceStatus = async ( req, res ) => {
|
|
3258
3283
|
try {
|
|
3259
3284
|
if ( !req.params?.invoiceId ) {
|
package/src/hbs/estimatePdf.hbs
CHANGED
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -881,6 +881,10 @@
|
|
|
881
881
|
align-self: stretch;
|
|
882
882
|
flex-shrink: 0;
|
|
883
883
|
position: relative;
|
|
884
|
+
/* Keep the whole totals block (Sub Total ... Total In Words) on a
|
|
885
|
+
single page so the amount-in-words can't be split across pages. */
|
|
886
|
+
break-inside: avoid;
|
|
887
|
+
page-break-inside: avoid;
|
|
884
888
|
}
|
|
885
889
|
|
|
886
890
|
.frame-2394 {
|
|
@@ -1011,6 +1015,8 @@
|
|
|
1011
1015
|
justify-content: flex-start;
|
|
1012
1016
|
flex-shrink: 0;
|
|
1013
1017
|
position: relative;
|
|
1018
|
+
break-inside: avoid;
|
|
1019
|
+
page-break-inside: avoid;
|
|
1014
1020
|
}
|
|
1015
1021
|
|
|
1016
1022
|
.text13 {
|
package/src/hbs/invoicePdf1.hbs
CHANGED
|
@@ -844,6 +844,10 @@
|
|
|
844
844
|
align-self: stretch;
|
|
845
845
|
flex-shrink: 0;
|
|
846
846
|
position: relative;
|
|
847
|
+
/* Keep the whole totals block (Sub Total ... Total In Words) on a
|
|
848
|
+
single page so the amount-in-words can't be split across pages. */
|
|
849
|
+
break-inside: avoid;
|
|
850
|
+
page-break-inside: avoid;
|
|
847
851
|
}
|
|
848
852
|
|
|
849
853
|
.frame-2394 {
|
|
@@ -974,6 +978,8 @@
|
|
|
974
978
|
justify-content: flex-start;
|
|
975
979
|
flex-shrink: 0;
|
|
976
980
|
position: relative;
|
|
981
|
+
break-inside: avoid;
|
|
982
|
+
page-break-inside: avoid;
|
|
977
983
|
}
|
|
978
984
|
|
|
979
985
|
.text13 {
|