diff --git a/backend/src/services/contractService.js b/backend/src/services/contractService.js index 4eae5f6f..e443b4ef 100644 --- a/backend/src/services/contractService.js +++ b/backend/src/services/contractService.js @@ -765,7 +765,9 @@ async function createContract(payload, adminId) { const hasEventCols = await hasColumnCached('contracts', 'event_name'); 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 = { contract_number: contractNumber, customer_account_id: payload.customerAccountId, @@ -1594,7 +1596,9 @@ async function createFromQuote(quoteId, adminId) { const hasContractEventCols = await hasColumnCached('contracts', 'event_name'); 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 = { contract_number: contractNumber, customer_account_id: quote.customer_account_id, diff --git a/backend/src/services/invoiceService.js b/backend/src/services/invoiceService.js index c50fe62f..ab02c247 100644 --- a/backend/src/services/invoiceService.js +++ b/backend/src/services/invoiceService.js @@ -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); diff --git a/backend/src/services/quoteService.js b/backend/src/services/quoteService.js index b93c7528..0c993ca0 100644 --- a/backend/src/services/quoteService.js +++ b/backend/src/services/quoteService.js @@ -491,7 +491,11 @@ async function createQuote(payload, adminId) { const bank = await businessProfileService.resolveBankAccountForCurrency(currency, payload.businessBankAccountId); 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 = { quote_number: quoteNumber, customer_account_id: payload.customerAccountId,