Skip to main content
Every gig invoice is a normal Invoice with InvoiceLine rows (the #3039 flow: InvoiceEditor, invoiceForm.ts, db/invoices.ts). A line that bills something Soundcheck tracks elsewhere carries a source key in invoice_lines.source_key. The key is the only link between an invoice line and its origin. Do not add a join table from a feature table to invoices.

Key grammar

Addendum and trip ids are 1–64 of A-Z a-z 0-9 _ -. Categories are 1–40 of a-z 0-9 _ -. A key is at most 160 characters. invoiceSourceGroupOf(key) names the invoice a key is drafted onto: the balance and every addendum share balance, every category of a trip shares buyout:<trip id>, and the rest stand alone.

Guarantees

  • One live invoice per source per gig. Creating or editing an invoice whose lines carry source keys takes the gig’s billing lock (lockEventBilling) and refuses a key already on another non-void invoice with 409 INVOICE_SOURCE_TAKEN. Voiding the invoice frees the key.
  • A key appears once per invoice. The parser rejects a second line with the same key.
  • Unknown keys are rejected at the API boundary (parseInvoiceFields) with 400.
  • Overtime keys must name a POSTED entry on the same gig.
  • Sending, voiding, receipts and receivables are the ordinary invoice lifecycle. A source key changes none of them.
The format is enforced in TypeScript, not by a database constraint. Always build and read keys through invoiceSource.ts.

API

Pure helpers, api-events/src/systems/invoices/invoiceSource.ts:
systems/billing-schedule/billingPlan.ts re-exports the shorthand builders addendumSourceKey, overtimeSourceKey, buyoutSourceKey, buyoutSourcePrefix and the DEPOSIT_1_SOURCE, DEPOSIT_2_SOURCE and BALANCE_SOURCE constants. Reads, api-events/src/db/billingCharges.ts:
  • listInvoiceSourceLinks(eventId, { sourceKeyPrefix?, includeVoid? }, client?) returns one row per sourced line: source_key, invoice_line_id, invoice_id, invoice_number, invoice_status, currency, amount, ledger_item_id. Void invoices are left out unless includeVoid is set.
  • listBillingCharges(eventId) returns the newest charge per key, preferring a non-void invoice, with its kind and due rule. GET /api/v1/events/:eventId/billing builds the schedule from it.
HTTP: POST and PUT /api/v1/events/:eventId/invoices[/:invoiceId] accept lines[].source_key. Invoice reads return it on each line.

Trip buyouts

  1. Build the draft lines with buildBuyoutInvoicePrefill(summary) (systems/billing-schedule/buyoutInvoiceAdapter.ts). It returns ready with one charge per priced category, each carrying buyoutSourceKey(tripId, category), or incomplete / invalid.
  2. Create the invoice on the Trip’s primary gig through the normal invoice create (createInvoiceOperation, or POST /api/v1/events/:eventId/invoices), with source_key on every line. A second live invoice for the same trip category gets 409 INVOICE_SOURCE_TAKEN.
  3. Find the trip’s invoices with listInvoiceSourceLinks(primaryEventId, { sourceKeyPrefix: buyoutSourcePrefix(tripId) }). The trailing colon in the prefix keeps trip-1 from matching trip-10.
The key does not record a trip revision. To detect a stale invoice, compare each link’s amount with the trip’s current buyout for that category. The billing view has no trip summary, so it reports buyout charges with planned_cents: null and cannot flag the difference itself.