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:
@@ -765,7 +765,9 @@ async function createContract(payload, adminId) {
|
|||||||
const hasEventCols = await hasColumnCached('contracts', 'event_name');
|
const hasEventCols = await hasColumnCached('contracts', 'event_name');
|
||||||
|
|
||||||
return await db.transaction(async (trx) => {
|
return await db.transaction(async (trx) => {
|
||||||
const contractNumber = await nextContractNumber();
|
// Pass trx so the sequence claim joins our outer transaction —
|
||||||
|
// SQLite deadlocks otherwise (1-connection default).
|
||||||
|
const contractNumber = await nextContractNumber(trx);
|
||||||
const row = {
|
const row = {
|
||||||
contract_number: contractNumber,
|
contract_number: contractNumber,
|
||||||
customer_account_id: payload.customerAccountId,
|
customer_account_id: payload.customerAccountId,
|
||||||
@@ -1594,7 +1596,9 @@ async function createFromQuote(quoteId, adminId) {
|
|||||||
const hasContractEventCols = await hasColumnCached('contracts', 'event_name');
|
const hasContractEventCols = await hasColumnCached('contracts', 'event_name');
|
||||||
|
|
||||||
return await db.transaction(async (trx) => {
|
return await db.transaction(async (trx) => {
|
||||||
const contractNumber = await nextContractNumber();
|
// Pass trx so the sequence claim joins our outer transaction —
|
||||||
|
// SQLite deadlocks otherwise (1-connection default).
|
||||||
|
const contractNumber = await nextContractNumber(trx);
|
||||||
const contractRow = {
|
const contractRow = {
|
||||||
contract_number: contractNumber,
|
contract_number: contractNumber,
|
||||||
customer_account_id: quote.customer_account_id,
|
customer_account_id: quote.customer_account_id,
|
||||||
|
|||||||
@@ -812,7 +812,9 @@ async function createInvoice(payload, adminId, trx = db) {
|
|||||||
// has been ruled out. Previously this was at the top of the function
|
// has been ruled out. Previously this was at the top of the function
|
||||||
// which leaked one number per multi-installment save (the spawner
|
// which leaked one number per multi-installment save (the spawner
|
||||||
// claims its own numbers and never used this one).
|
// 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 = {
|
const row = {
|
||||||
invoice_number: invoiceNumber,
|
invoice_number: invoiceNumber,
|
||||||
customer_account_id: payload.customerAccountId,
|
customer_account_id: payload.customerAccountId,
|
||||||
@@ -976,7 +978,7 @@ async function spawnInstallmentInvoices({ trx, eventId, quoteId, customer, curre
|
|||||||
const rowStatus = isDeliveryTrigger ? 'pending_delivery' : 'scheduled';
|
const rowStatus = isDeliveryTrigger ? 'pending_delivery' : 'scheduled';
|
||||||
const rowScheduledSendAt = isDeliveryTrigger ? null : scheduledSendAt;
|
const rowScheduledSendAt = isDeliveryTrigger ? null : scheduledSendAt;
|
||||||
|
|
||||||
const invoiceNumber = await nextInvoiceNumber();
|
const invoiceNumber = await nextInvoiceNumber(trx);
|
||||||
const dueDate = computeDueDate(scheduledSendAt, resolvedNetDays).toISOString().slice(0, 10);
|
const dueDate = computeDueDate(scheduledSendAt, resolvedNetDays).toISOString().slice(0, 10);
|
||||||
|
|
||||||
const row = {
|
const row = {
|
||||||
@@ -2049,7 +2051,9 @@ async function createStorno(originalId, adminId, trx = db) {
|
|||||||
// Generate the Storno's sequence number from the same gap-free
|
// Generate the Storno's sequence number from the same gap-free
|
||||||
// series as regular invoices (single sequence — decision locked
|
// series as regular invoices (single sequence — decision locked
|
||||||
// with the maintainer; satisfies §14 (4) Nr. 4 UStG).
|
// 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 now = new Date();
|
||||||
const issueDate = now.toISOString().slice(0, 10);
|
const issueDate = now.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
|||||||
@@ -491,7 +491,11 @@ async function createQuote(payload, adminId) {
|
|||||||
const bank = await businessProfileService.resolveBankAccountForCurrency(currency, payload.businessBankAccountId);
|
const bank = await businessProfileService.resolveBankAccountForCurrency(currency, payload.businessBankAccountId);
|
||||||
|
|
||||||
return await db.transaction(async (trx) => {
|
return await db.transaction(async (trx) => {
|
||||||
const quoteNumber = await nextQuoteNumber();
|
// SQLite's 1-connection default deadlocks when claimNextSequence
|
||||||
|
// opens its own micro-transaction inside this outer one — thread
|
||||||
|
// trx so both run on the same connection. Postgres tolerates
|
||||||
|
// either form but the consistency is worth it.
|
||||||
|
const quoteNumber = await nextQuoteNumber(trx);
|
||||||
const row = {
|
const row = {
|
||||||
quote_number: quoteNumber,
|
quote_number: quoteNumber,
|
||||||
customer_account_id: payload.customerAccountId,
|
customer_account_id: payload.customerAccountId,
|
||||||
|
|||||||
Reference in New Issue
Block a user