tango-app-api-payment-subscription 3.5.30 → 3.5.31

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.30",
3
+ "version": "3.5.31",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -15,7 +15,7 @@ import * as basepricingService from '../services/basePrice.service.js';
15
15
  import * as paymentAccountService from '../services/paymentAccount.service.js';
16
16
  import { symbolFor, roundAmount, wordFor } from '../utils/currency.js';
17
17
  import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
18
- import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
18
+ import { findOneApplicationDefault, nextCounterValue } from '../services/applicationDefault.service.js';
19
19
  import * as assignedStoreService from '../services/assignedStore.service.js';
20
20
  import * as bankTransactionService from '../services/bankTransaction.service.js';
21
21
  import { getInrRate, getAdditionalProducts } from './brandsBilling.controller.js';
@@ -3252,15 +3252,29 @@ async function transitionInvoiceStatus( req, res, fromStatus, toStatus ) {
3252
3252
  if ( fromStatus === 'pendingFinance' && String( invoice.invoice || '' ).startsWith( 'DRAFT-' ) ) {
3253
3253
  const FY = getCurrentFinancialYear();
3254
3254
  const prefix = invoicePrefixFor( invoice.advanceInvoice, invoice.advancePeriod );
3255
- const previous = await invoiceService.findandsort(
3255
+ // Reserve the next number ATOMICALLY. Bulk finance-approval fires many
3256
+ // requests in parallel; the old read-max-then-write approach let them all
3257
+ // read the same max and mint the SAME number. nextCounterValue uses an
3258
+ // atomic $inc on a per-series counter, so simultaneous callers each get a
3259
+ // distinct number. The counter is lazily seeded (once, on first use) from
3260
+ // the current DB max of this prefix+FY series so existing numbers are
3261
+ // never reused.
3262
+ // Numeric max of existing invoiceIndex for this series. invoiceIndex is a
3263
+ // STRING in the schema and historical values are zero-padded ('00569'),
3264
+ // so a Mongo sort would compare lexically — compute the true numeric max
3265
+ // in JS instead. Used only to seed the counter on its first use.
3266
+ const previous = await invoiceService.find(
3256
3267
  { invoice: { $regex: `^${prefix}${FY}-` } },
3257
- {},
3258
- { invoiceIndex: -1 },
3268
+ { invoiceIndex: 1 },
3259
3269
  );
3260
- let realNo = 1;
3261
- if ( previous && previous.length > 0 && previous[0].invoiceIndex != null ) {
3262
- realNo = Number( previous[0].invoiceIndex ) + 1;
3270
+ let dbMax = 0;
3271
+ for ( const p of ( previous || [] ) ) {
3272
+ const n = Number( p.invoiceIndex );
3273
+ if ( !Number.isNaN( n ) && n > dbMax ) {
3274
+ dbMax = n;
3275
+ }
3263
3276
  }
3277
+ const realNo = await nextCounterValue( 'invoiceCounter', `${prefix}${FY}`, dbMax );
3264
3278
  update.invoice = `${prefix}${FY}-${String( realNo ).padStart( 5, '0' )}`;
3265
3279
  update.invoiceIndex = realNo;
3266
3280
  update.tempIndex = null;
@@ -11,3 +11,32 @@ export async function upsertApplicationDefault( query, payload ) {
11
11
  { new: true, upsert: true },
12
12
  );
13
13
  }
14
+
15
+ // Atomically reserve the next value for a named counter and return it.
16
+ // Uses a single findOneAndUpdate($inc) — MongoDB serialises concurrent
17
+ // increments on the same document, so simultaneous callers (e.g. bulk invoice
18
+ // finance-approval) each get a distinct number, eliminating the read-max-then-
19
+ // write race that produced duplicate invoice numbers.
20
+ //
21
+ // `subType` scopes the counter (e.g. 'INV-26-27'). `seedFrom` is the current
22
+ // real-world max (e.g. the highest existing invoiceIndex for that series); it
23
+ // initialises the counter ONLY on first creation via $setOnInsert, so existing
24
+ // numbers are never re-used. Returns the reserved integer.
25
+ export async function nextCounterValue( type, subType, seedFrom = 0 ) {
26
+ // Two-step, both atomic:
27
+ // 1) Ensure the counter doc exists, seeded to the current max WITHOUT
28
+ // incrementing (upsert + $setOnInsert). If it already exists this is a
29
+ // no-op and never lowers a counter that has moved past the seed.
30
+ await model.applicationDefaultModel.updateOne(
31
+ { type, subType },
32
+ { $setOnInsert: { type, subType, data: { seq: Number( seedFrom ) || 0 } } },
33
+ { upsert: true },
34
+ );
35
+ // 2) Atomically claim the next value.
36
+ const doc = await model.applicationDefaultModel.findOneAndUpdate(
37
+ { type, subType },
38
+ { $inc: { 'data.seq': 1 } },
39
+ { new: true },
40
+ );
41
+ return doc?.data?.seq;
42
+ }