fix(crm): thread trx through sequence-claim sites to unblock SQLite

Reviewer feedback on #555: nextQuoteNumber inside createQuote's
db.transaction was called without passing the outer trx, so
claimNextSequence opened its own connection — Postgres tolerated this
via the pool, SQLite (1-connection default) deadlocked on every quote
creation.
Audited the same pattern across invoiceService + contractService and
found five more matching call sites:
  - createInvoice (single-row path after installment auto-route)
  - spawnInstallmentInvoices (per-sibling claim inside the loop)
  - createStorno
  - createContract
  - createFromQuote
All now thread trx through to nextXxxNumber → claimNextSequence so
the claim joins the caller's transaction on both engines.
convertToInvoiceOnly's Path B (standalone-contract) is the lone
remaining nextInvoiceNumber() call without trx — that path isn't
wrapped in a transaction at all (separate concern: sequence-number
leak on insert failure, tracked separately).
This commit is contained in:
Luca
2026-05-27 22:08:38 +02:00
parent 5ce0b6edc3
commit d1aecaa180
3 changed files with 18 additions and 6 deletions
+7 -3
View File
@@ -812,7 +812,9 @@ async function createInvoice(payload, adminId, trx = db) {
// has been ruled out. Previously this was at the top of the function
// which leaked one number per multi-installment save (the spawner
// claims its own numbers and never used this one).
const invoiceNumber = await nextInvoiceNumber();
// Pass trx so the sequence claim joins our outer transaction —
// SQLite deadlocks otherwise (1-connection default).
const invoiceNumber = await nextInvoiceNumber(trx);
const row = {
invoice_number: invoiceNumber,
customer_account_id: payload.customerAccountId,
@@ -976,7 +978,7 @@ async function spawnInstallmentInvoices({ trx, eventId, quoteId, customer, curre
const rowStatus = isDeliveryTrigger ? 'pending_delivery' : 'scheduled';
const rowScheduledSendAt = isDeliveryTrigger ? null : scheduledSendAt;
const invoiceNumber = await nextInvoiceNumber();
const invoiceNumber = await nextInvoiceNumber(trx);
const dueDate = computeDueDate(scheduledSendAt, resolvedNetDays).toISOString().slice(0, 10);
const row = {
@@ -2049,7 +2051,9 @@ async function createStorno(originalId, adminId, trx = db) {
// Generate the Storno's sequence number from the same gap-free
// series as regular invoices (single sequence — decision locked
// with the maintainer; satisfies §14 (4) Nr. 4 UStG).
const stornoNumber = await nextInvoiceNumber();
// Pass trx so the sequence claim joins the caller's transaction —
// SQLite deadlocks otherwise (1-connection default).
const stornoNumber = await nextInvoiceNumber(trx);
const now = new Date();
const issueDate = now.toISOString().slice(0, 10);