fix(workflows): held booking invoices are 'scheduled', not 'pending_delivery' — so send_document can issue them
A quote with no explicit payment timing falls back to a single after_delivery installment. spawnInstallmentInvoices marked those 'pending_delivery' even in hold mode, so the booking flow's send_document -> sendInvoice threw 'Cannot send invoice with status pending_delivery', the run failed, and no invoice email went out (the symptom: approve the quote->invoice flow, receive nothing). In hold mode the flow's review gate + explicit send_document IS the delivery release, so a held invoice is always 'scheduled' (editable + sendable) regardless of trigger; scheduled_send_at stays null so the scheduler never auto-sends it. Non-hold after_delivery invoices keep 'pending_delivery' as before. Adds a regression test (default after_delivery term -> draft -> scheduled+null).
This commit is contained in:
@@ -76,6 +76,29 @@ describe('booking cutover — draft invoices on hold', () => {
|
||||
expect(q.converted_event_id).toBe(res.eventId);
|
||||
});
|
||||
|
||||
it('draft mode with the DEFAULT (after_delivery) payment term yields a SENDABLE scheduled invoice, not pending_delivery', async () => {
|
||||
// Reproduces the booking_invoice_only flow on a quote with no explicit
|
||||
// payment timing: the default installment is after_delivery, which would
|
||||
// otherwise be pending_delivery — a status sendInvoice (send_document) rejects.
|
||||
const dealUuid = crypto.randomUUID();
|
||||
const [quoteId] = await db('quotes').insert({
|
||||
quote_number: `Q-${dealUuid.slice(0, 8)}`,
|
||||
customer_account_id: customerId,
|
||||
status: 'accepted',
|
||||
currency: 'CHF',
|
||||
issue_date: '2026-01-01',
|
||||
net_amount_minor: 50000, vat_amount_minor: 0, shipping_amount_minor: 0, total_amount_minor: 50000,
|
||||
// No payment_term_snapshot → spawnInstallmentInvoices falls back to a single
|
||||
// 100% after_delivery installment.
|
||||
deal_uuid: dealUuid,
|
||||
created_by_admin_id: adminId,
|
||||
});
|
||||
const res = await quoteService.convertToInvoiceOnly(quoteId, adminId, { draft: true });
|
||||
const inv = await db('invoices').where({ id: res.invoiceIds[0] }).first();
|
||||
expect(inv.status).toBe('scheduled'); // sendInvoice accepts this
|
||||
expect(inv.scheduled_send_at == null).toBe(true); // still held — no auto-send
|
||||
});
|
||||
|
||||
it('reserve_date path (convertToEvent skipInvoices) creates a draft event with NO invoices', async () => {
|
||||
const quoteId = await acceptedQuote();
|
||||
const res = await quoteService.convertToEvent(quoteId, adminId, { hold: true, skipInvoices: true });
|
||||
|
||||
@@ -1077,11 +1077,15 @@ async function spawnInstallmentInvoices({ trx, eventId, quoteId, customer, curre
|
||||
// status `scheduled`, so they sit idle until the admin clicks
|
||||
// "Release for delivery" on the invoice detail page.
|
||||
const isDeliveryTrigger = inst.trigger === 'after_delivery';
|
||||
const rowStatus = isDeliveryTrigger ? 'pending_delivery' : 'scheduled';
|
||||
// `hold` (workflow draft-seam): create the invoice but leave scheduled_send_at
|
||||
// NULL so the scheduler never auto-sends it — a draft awaiting an explicit
|
||||
// send_document after the admin's review gate. Status stays 'scheduled' so
|
||||
// it's editable and sendInvoice can later issue it.
|
||||
// `hold` (workflow draft-seam): the booking flow's review gate + explicit
|
||||
// send_document IS the release, so a held invoice is always `scheduled`
|
||||
// (editable + sendable via sendInvoice) regardless of trigger — never
|
||||
// `pending_delivery`, which sendInvoice refuses. Without hold, an
|
||||
// after_delivery invoice stays `pending_delivery` as before.
|
||||
const rowStatus = (isDeliveryTrigger && !hold) ? 'pending_delivery' : 'scheduled';
|
||||
// Held invoices carry no scheduled_send_at so the scheduler never auto-sends
|
||||
// them — they wait for send_document. after_delivery rows are likewise null
|
||||
// (the scheduler can't infer a delivery date).
|
||||
const rowScheduledSendAt = (isDeliveryTrigger || hold) ? null : scheduledSendAt;
|
||||
|
||||
const invoiceNumber = await nextInvoiceNumber(trx);
|
||||
|
||||
Reference in New Issue
Block a user