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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-payment-subscription",
3
- "version": "3.5.21",
3
+ "version": "3.5.22",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1944,7 +1944,11 @@ export async function billingSummary( req, res ) {
1944
1944
  csm: [ ...( csmByClient.get( key ) || [] ) ].join( ', ' ),
1945
1945
  revenueMonths: {},
1946
1946
  billedStoresMonths: {},
1947
- installationFee: 0,
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.installationFee += ( inv.installationInr || 0 ) + ( inv.installationUsd || 0 ) * usdRate;
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
- const installationOut = r.installationFee ? Math.round( r.installationFee ) : null;
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
- groupName: e.groupName || '',
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
- const amount = Math.round( Number( b.amount ) || 0 );
215
- let totalAmount = Math.round( Number( b.totalAmount ) || 0 );
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 = Math.round( amount * 1.18 );
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: b.currency || ( client?.paymentInvoice?.currencyType === 'dollar' ? 'dollar' : 'inr' ),
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
- const amount = Math.round( Number( b.amount ) || 0 );
277
- let totalAmount = Math.round( Number( b.totalAmount ) || 0 );
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 = Math.round( amount * 1.18 );
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: b.currency || existing.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 = inWords( invoiceInfo.totalAmount );
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 = inWords( invoiceInfo.totalAmount );
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 = inWords( invoiceInfo.totalAmount );
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 ) {
@@ -1423,7 +1423,6 @@
1423
1423
  </div>
1424
1424
  </div>
1425
1425
  <div class="_1234"># {{estimate}}</div>
1426
- <div><span class="pill pill-{{status}}">{{statusLabel}}</span></div>
1427
1426
 
1428
1427
  </div>
1429
1428
 
@@ -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 {
@@ -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 {