Skip to main content

Billing::InvoiceLine

Rename Notice

This model was previously named Billing::InvoiceItem with table billing_invoice_items. Phase 2 renames it to Billing::InvoiceLine / billing_invoice_lines.

Why the rename? The term "line" is the industry standard for a row on a commercial document (Xero, Stripe, and QuickBooks all use "line"). The term "item" implies a standalone entity that exists independently, whereas a "line" unambiguously belongs to a document. This rename aligns Jod's terminology with the industry and prevents confusion when engineers work with external accounting systems.

Impact: Requires a migration to rename the table. All existing references to billing_invoice_items in code and documentation must be updated.


Purpose

A Billing::InvoiceLine is a single row on an invoice representing one sold item. Each line is a snapshot of what was sold — capturing the product description, quantity, unit price, applicable tax, and how many entitlement credits to grant on posting.

Lines are the bridge between the commercial sale (what appears on the customer's invoice) and entitlement granting (what the InvoicePosting uses to write ledger entries).


Responsibilities

  • Snapshot pricing, tax, and grant quantities at invoice creation time — these values never change after creation, even if the underlying product or price is updated
  • Encode line_type to distinguish the principal (what the customer is buying) from the platform fee (Jod's margin, gig credits only)
  • Hold units_to_grant so the posting step knows exactly how many credits to grant without recomputing from the product catalog
  • Store platform_fee_rate_bps (gig only) so the lot creation step in posting has the frozen rate

Model Context

ContextDetails
AggregateChild of Billing::Invoice
LayerCommercial Documents
UpstreamBilling::Invoice (parent), Billing::Product (snapshot source), Billing::ProductPrice (snapshot source)
DownstreamBilling::InvoicePosting reads line_type and units_to_grant to decide grant behavior

Table Schema

Table name: billing_invoice_lines (renamed from billing_invoice_items in Phase 2)

ColumnTypeConstraintsNotes
idbigintPK, auto-increment
uuidstringNOT NULL, UNIQUEExternal-safe identifier
billing_invoice_idbigintNOT NULL, FK → billing_invoices.idParent invoice
billing_product_idbigintNOT NULL, FK → billing_products.idWhat product was sold — snapshot reference
billing_product_price_idbigintNOT NULL, FK → billing_product_prices.idWhich price was used — snapshot reference
descriptionstring:principal snapshots billing_products.name; :platform_fee is always the literal string "Platform Fee"
quantityintegerHow many units purchased
unit_price_centsintegerSnapshot from billing_product_prices.unit_price_cents — never overridden by an agreement
amount_centsintegerquantity × unit_price_cents (gross — never reduced by a discount; see discount_cents)
tax_centsintegerSee tax policy below
units_to_grantintegerquantity × billing_products.grants_units_per_quantity. Always 0 for :platform_fee lines
line_typeenum stringNOT NULLrails_enum(:principal, :platform_fee)
platform_fee_rate_bpsintegernullableGig only — the frozen platform fee rate. NULL for placement and all non-gig products
discount_centsintegerNOT NULL, default 0Amount discounted off this line via the agreement's discount_rate term. Lands on whichever line is Jod's own revenue: the :platform_fee line for gig, the :principal line for placement (see Tax Policy). Always 0 on the gig :principal line.
discount_rate_bpsintegernullableThe discount_rate term's basis points used to compute discount_cents. NULL when no discount applies
created_attimestamptz
updated_attimestamptz

Line Types

line_typeWhat it isunits_to_grantTax
:principalThe entitlement being purchased (credits, stored value)> 0Placement: full GST on amount_cents. Gig: 0% (tax-exempt)
:platform_feeJod's platform margin (gig credits only)0 (money only)Full GST on amount_cents

A Placement Credits invoice has exactly 1 line: :principal

A Gig Credits invoice has exactly 2 lines: 1 × :principal + 1 × :platform_fee


Tax Policy

ProductLine TypeTax BaseTax Rate
Placement Credits (SG):principalamount_cents - discount_cents9% GST (SR code)
Gig Credits — principal (SG):principal00% (tax-exempt stored value)
Gig Credits — platform fee (SG):platform_feeamount_cents - discount_cents9% GST (SR code)

When an agreement's discount_rate term applies, tax is computed on the post-discount amount of whichever line carries the discount — the :platform_fee line for gig, the :principal line for placement — not that line's gross amount_cents. The line that does not carry the discount is unaffected (gig :principal is always tax-exempt regardless; placement has no :platform_fee line).

For Indonesia (future):

  • Placement: 11% PPN (PPN_STD)
  • Gig principal: exempt
  • Gig platform fee: 11% PPN

Key Indexes

IndexColumnsTypePurpose
uuidUNIQUEExternal lookup
billing_invoice_idAll lines for an invoice

Snapshot Invariants

All values on an invoice line are immutable after creation. They represent the commercial truth at the moment of sale.

FieldSnapshotted FromWhy snapshot?
descriptionbilling_products.name (:principal) or the literal "Platform Fee" (:platform_fee)Product name could change; invoice must show what was sold at the time
unit_price_centsbilling_product_prices.unit_price_cents — always the catalog price, agreements never override itPrice could change; invoice must show the contracted price
tax_centsComputed from the post-discount amount × tax_rateTax rate could change; invoice must show tax at time of sale
units_to_grantquantity × grants_units_per_quantityGrant rate could change; posting must use the rate at sale time
platform_fee_rate_bpsAgreement's fee_rate term OR ProductPrice.platform_fee_rate_bpsFee rate is negotiated per agreement; must be frozen at invoice creation
discount_cents / discount_rate_bpsAgreement's discount_rate term, applied to the :platform_fee line for gig or the :principal line for placementDiscount is negotiated per agreement; must be frozen at invoice creation. nil/0 when no discount_rate term exists for the entitlement — there is no catalog fallback

How InvoicePosting Uses InvoiceLine

When an invoice is posted (after payment is verified and invoice reaches paid), the InvoicePosting service iterates over all InvoiceLines and acts based on line_type:

For each InvoiceLine:
if line_type == :principal AND entitlement == :placement:
→ LedgerEntry(grant, available_delta: +units_to_grant, deferred_revenue_delta: +amount_cents)

if line_type == :principal AND entitlement == :gig: [Phase 3]
→ LedgerEntry(grant) + EntitlementLot(platform_fee_rate_bps: platform_fee_rate_bps)

if line_type == :platform_fee AND entitlement == :gig: [Phase 3]
→ LedgerEntry(platform_fee_deferred_delta: +amount_cents) — no units granted

Invariants

  1. Every InvoiceLine must belong to exactly one Billing::Invoice
  2. units_to_grant == 0 for all :platform_fee lines (enforced by DB CHECK constraint check_platform_fee_lines_grant_no_units, since Phase 3 posting grants credits straight from this column — a violated row means free credits for a customer)
  3. platform_fee_rate_bps IS NOT NULL for gig lines; IS NULL for all other lines
  4. amount_cents == quantity × unit_price_cents (enforced at creation)
  5. tax_cents follows the tax policy for the product's entitlement type and the seller's tax regime
  6. All snapshot fields are immutable after creation — changes to products, prices, or agreements cannot mutate existing lines
  7. A Placement Credits invoice has exactly 1 line (:principal)
  8. A Gig Credits invoice has exactly 2 lines (1 × :principal, 1 × :platform_fee) — enforced by TeamCreateValidator/TeamUpdateValidator rejecting any invoice with more than one submitted line param (BuildLinesService still expands a single gig line param into 2 persisted rows)
  9. discount_cents is always 0 on the gig :principal line — the discount reduces Jod's own platform fee margin, never the wage value owed to the buyer/company. For placement (no :platform_fee line), the discount lands on :principal instead.

Design Deviation from Source

What ChangedSource It Differs FromRationale
Rename billing_invoice_itemsbilling_invoice_lines (table)DBML, Ali's use casesIndustry-standard term. See rename notice at top of this page.
Rename Billing::InvoiceItemBilling::InvoiceLine (model)Existing model namingConsistency with table rename