feat(workflows): wire booking document actions (prepare_invoice/contract + send_document)
Implements the draft-seam booking cutover so the booking_invoice_only flow
becomes enableable. The booking flows trigger on quote.accepted, so the run
entity is the quote:
- prepare_invoice: convertToInvoiceOnly({draft:true}) creates the invoice(s)
on HOLD (scheduled_send_at NULL, status stays 'scheduled') so the scheduler
never auto-sends before the review gate; crash-recovery recovers drafts by
the quote's deal_uuid. Stores ids in ctx.vars.preparedInvoiceIds.
- prepare_contract: createFromQuote (idempotent via converted_contract_id).
- send_document: dispatches the prepared draft (invoice -> sendInvoice each id,
contract -> sendContract).
- resolveActor: quote creator -> workflow creator -> first admin.
- prepare_contract/prepare_invoice/send_document removed from the enable-guard
list; prepare_event/prepare_quote/prepare_gallery/reserve_date still guarded,
so booking_full/booking_simple stay blocked until the event-path increment.
Fixes a latent single-connection SQLite deadlock these unattended paths would
hit: getAppSetting/logActivity/adminActor read or write the global db, which
deadlocks when issued inside an open knex transaction. Thread the active trx
through getAppSetting, logActivity, nextInvoiceNumber, nextContractNumber, the
spawnInstallmentInvoices audit log, and hoist adminActor before createFromQuote's
transaction. convertToInvoiceOnly now logs after commit and returns invoiceIds.
Adds bookingCutover integration test (hold-mode null send-at, normal scheduled
contrast, contract path no-deadlock) and a route test that the now-implemented
booking invoice actions can be enabled.
This commit is contained in:
@@ -25,8 +25,11 @@ const { db } = require('../database/db');
|
||||
* JSON.parse on the way out. Falls back to the raw string on
|
||||
* malformed JSON so legacy text values still work.
|
||||
*/
|
||||
async function getAppSetting(key, defaultValue = null) {
|
||||
const row = await db('app_settings').where({ setting_key: key }).first();
|
||||
async function getAppSetting(key, defaultValue = null, conn = db) {
|
||||
// Callers reading from inside a knex transaction must pass that trx as
|
||||
// `conn`, otherwise the global-`db` read grabs a second connection from
|
||||
// the single-connection SQLite pool while the trx holds it → deadlock.
|
||||
const row = await conn('app_settings').where({ setting_key: key }).first();
|
||||
if (!row || row.setting_value == null) return defaultValue;
|
||||
try {
|
||||
return JSON.parse(row.setting_value);
|
||||
|
||||
Reference in New Issue
Block a user