Invoice Item
InvoiceItem represents a single line item on an invoice — a discrete charge for a product or service with its own quantity, price, tax, and period metadata. It belongs exclusively to one Invoice via a mandatory many-to-one relation. Key associations are: Invoice (parent document), LedgerAccount (accounting chart of accounts classification), TaxRate (the well-catalog tax rate that was applied), and Media (an optional supporting document such as an image or receipt). The accounting_classification JSONB field carries the AI-produced Well-taxonomy posting intent used by the journal-entry builder to classify the line for double-entry accounting.
| Naming | Value |
|---|---|
| Object | Invoice Item |
Resource type (JSON:API type) | invoice_item |
| Collection / records root | invoice_items |
| REST base | /v1/invoice-items |
| Entity class | InvoiceItem |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/invoice-items | ✅ Implemented |
| Retrieve | GET /v1/invoice-items/{id} | ✅ Implemented |
| Create | POST /v1/invoice-items | 🟡 Planned |
| Update | PATCH /v1/invoice-items/{id} | 🟡 Planned |
| Delete | DELETE /v1/invoice-items/{id} | 🟡 Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| invoice_item_id | string, UUID | ✅ Yes | unique; generated by gen_random_uuid() at creation | — | Public stable identifier for this invoice line item. Used in all API surfaces and references. |
| line_id | string | ✅ Yes | max length 50; table-wide composite-unique with invoice (no two lines — active or deleted — on the same invoice share a line_id) | — | Provider-assigned or pipeline-generated identifier for this line within the parent invoice. Used to deduplicate re-ingested lines. The unique constraint is not partial: a soft-deleted line_id blocks reuse on the same invoice until the row is hard-deleted. |
| sku | string | ⚪ No | max length 100 | — | Stock-keeping unit or product code from the originating system (ERP, e-commerce, MCP connector). |
| name | string | ✅ Yes | max length 255 | — | Human-readable label for the line item. Mapped directly from the originating document's line description. |
| description | string | ⚪ No | max length 1000 | — | Extended free-text description of the line item, supplying additional context beyond the name. |
| unit_price | decimal(15,2) | ✅ Yes | CHECK unit_price >= 0 | — | Price per unit in the invoice's currency, before any discount or tax. Always non-negative. |
| currency | string, enum (CurrencyCodeEnum) | ✅ Yes | PostgreSQL native enum currency_code_enum | ISO 4217 three-letter codes, e.g. USD, EUR, GBP, JPY, CHF, CAD, AUD … +1 more | Currency of unit_price, line_total, tax_amount, discount, and quantity-derived amounts on this line. |
| unit | string, enum (InvoiceLineUnitEnum) | ⚪ No | PostgreSQL native enum invoice_line_unit_enum; nullable | EA, PC, SET, PR, DZ, C62, MIL, KT … +87 more | UN/CEFACT unit-of-measure code for the quantity on this line. Covers physical quantities, time, data, and service units. |
| quantity | decimal(15,2) | ⚪ No | CHECK quantity >= 0; nullable | — | Number of units. Stored as decimal(15,2) to accommodate fractional service quantities. Populated by the pipeline ingestion from connector data. |
| min_quantity | decimal(15,2) | ⚪ No | CHECK min_quantity >= 0; nullable | — | Minimum purchasable quantity for this line, used when the invoice carries tiered or min/max quantity brackets. |
| max_quantity | decimal(15,2) | ⚪ No | CHECK max_quantity >= min_quantity; nullable | — | Maximum purchasable quantity for this line. Must be >= min_quantity when both are present. |
| line_total | decimal(15,2) | ⚪ No | CHECK line_total >= 0; nullable | — | Total amount for this line (unit_price × quantity − discount), excl. tax. Populated by the pipeline; may differ from a client-computed product when rounding rules apply. |
| discount | decimal(15,2) | ⚪ No | CHECK discount >= 0; nullable; default 0 | — | Discount amount applied to this line, expressed as an absolute monetary value in the line's currency. Always non-negative. |
| tax_rate | decimal(5,2) | ⚪ No | CHECK tax_rate >= 0 AND tax_rate <= 100; nullable | 0.00 – 100.00 | Percentage rate of tax applied to this line, e.g. 20.00 for 20% VAT. Distinct from the applied_tax_rate relationship which links to the well-catalog tax rate entry. |
| tax_category | string, enum (TaxCategoryEnum) | ⚪ No | PostgreSQL native enum tax_category_enum; nullable | standard, reduced, super_reduced, zero_rated, exempt, reverse_charge, out_of_scope, government … +91 more | Semantic classification of the tax treatment on this line, aligned to EU/global tax-code taxonomy. |
| tax_scheme | string, enum (TaxSchemeEnum) | ⚪ No | PostgreSQL native enum tax_scheme_enum; nullable | VAT, EU_VAT, UK_VAT, MOSS_VAT, OSS_VAT, IOSS_VAT, GST, AU_GST … +83 more | The tax framework under which the line is taxed, e.g. EU_VAT, GST, US_STATE_TAX. Covers 90+ global tax schemes. |
| tax_amount | decimal(15,2) | ⚪ No | CHECK tax_amount >= 0; nullable | — | Absolute tax amount for this line in the line's currency, derived from unit_price × quantity × tax_rate / 100. |
| accounting_unit_price | decimal(15,2) | ⚪ No | nullable | — | Unit price converted to the workspace's functional accounting currency. Set by the FX-matching pipeline when the invoice currency differs from the workspace currency. |
| accounting_line_total | decimal(15,2) | ⚪ No | nullable | — | Line total converted to the workspace's functional accounting currency. Parallel to accounting_unit_price for full multi-currency double-entry support. |
| period_start | timestamp | ⚪ No | nullable | — | Start of the service period covered by this line item. Used for subscription, retainer, and recurring-service invoices. |
| period_end | timestamp | ⚪ No | nullable | — | End of the service period covered by this line item. Paired with period_start for accrual accounting period allocation. |
| accounting_classification | jsonb | ⚪ No | nullable; partial functional index on (accounting_classification->>'status') WHERE deleted_at IS NULL (idx_invoice_items_accounting_classification_status — defined in Migration20260525101000 only, not expressible via MikroORM decorators) | { status: 'ready', wellCoaVersion: string, intent: WellPostingIntent, rawFacts: InvoiceItemAccountingLLMFacts } | { status: 'needs_review', wellCoaVersion: string, reason: InvoiceItemAccountingReviewReason, rawFacts?: ..., details?: string } | AI-produced Well-taxonomy accounting classification for this line. Written by the journal-entry draft builder. status='ready' means a valid WellPostingIntent was resolved and the line can be posted to the ledger. status='needs_review' carries a reason code (missing_accounting_facts, llm_requested_review, unknown_semantic_role, unsupported_invoice_item_posting_kind, unsupported_invoice_item_transfer_role, invalid_accounting_qualifier, posting_intent_halt) and blocks automatic posting. |
| created_at | timestamp, 🔒 system | ✅ Yes | set once on insert via onCreate lifecycle hook | — | ISO 8601 timestamp recording when the invoice item row was first persisted. |
| updated_at | timestamp, 🔒 system | ⚪ No | set on insert and refreshed on every update via onCreate/onUpdate lifecycle hooks | — | ISO 8601 timestamp of the most recent write to this row. |
| deleted_at | timestamp | ⚪ No | nullable; soft-delete sentinel; partial indexes exclude rows where deleted_at IS NOT NULL | — | When set, marks this line as logically deleted. All active-record queries filter deleted_at IS NULL. Cleared only by an explicit restore operation. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| invoice | to-one (invoice) | ✅ Yes | The parent invoice to which this line belongs. Non-nullable ManyToOne to the Invoice entity. A line cannot exist without its parent. Indexed via idx_invoice_items_invoice_deleted (invoice + deleted_at) and the partial index idx_invoice_items_invoice_active (invoice_pk WHERE deleted_at IS NULL) for hot UPDATE paths during invoice-merge operations. |
| ledger_account | to-one (ledger_account) | ⚪ No | Optional link to the LedgerAccount (chart-of-accounts entry) that this line item has been assigned to. Set by the accounting classification pipeline or by a user override. Indexed via idx_invoice_items_ledger_account. |
| applied_tax_rate | to-one (tax_rate) | ⚪ No | Optional link to the Well-catalog TaxRate entry that was applied when computing the tax on this line. Distinct from the scalar tax_rate column which stores the raw percentage. Indexed via idx_invoice_items_applied_tax_rate. |
| media | to-one (media) | ⚪ No | Optional supporting document or image attached to this line (e.g. a product image, receipt scan, or delivery note). ManyToOne to the Media entity. Indexed via idx_invoice_items_media. |
System-computed
- invoice_item_id is generated by PostgreSQL gen_random_uuid() at insert time and is immutable thereafter.
- created_at is set once by the MikroORM onCreate lifecycle hook; it is never updated.
- updated_at is set by both onCreate and onUpdate hooks — it reflects the wall-clock time of the most recent write.
- deleted_at is null on creation. Setting it to a non-null timestamp constitutes a soft delete. The partial indexes idx_invoice_items_invoice_active and idx_invoice_items_accounting_classification_status both carry WHERE deleted_at IS NULL to exclude deleted rows from hot read paths.
- The composite unique constraint (invoice, line_id) is table-wide (not partial): it enforces that no two lines — active or soft-deleted — on the same invoice share the same line_id. A soft-deleted line_id cannot be reused for a new line on the same invoice without first hard-deleting the old row.
- accounting_unit_price and accounting_line_total are derived by the FX-matching pipeline (invoice.service.ts + fx-rate.service.ts) when the line currency differs from the workspace's functional currency. They are never computed by the API layer on a write request.
- accounting_classification is written exclusively by the invoice-journal-entry-draft builder (services/accounting/invoice-journal-entry-draft.builder.ts) and classifyInvoiceItemAccounting(). It is never set by a direct API mutation. Its status field is indexed via a JSONB functional partial index (migration-only; MikroORM decorators cannot express JSONB key expressions or partial predicates, so schema:fresh diverges from production on this index).
- discount defaults to 0 at the database level when not supplied by the connector.
Example
{
"data": {
"type": "invoice_item",
"id": "a3f7b2c1-84d9-4e10-b6e0-2f1d5c839740",
"attributes": {
"invoice_item_id": "a3f7b2c1-84d9-4e10-b6e0-2f1d5c839740",
"line_id": "line-001",
"sku": "SVC-CONSULTING-2026",
"name": "Strategic consulting — Q2 2026",
"description": "Monthly advisory retainer covering financial modelling and board prep.",
"unit_price": "4500.00",
"currency": "EUR",
"unit": "MON",
"quantity": "1.00",
"min_quantity": null,
"max_quantity": null,
"line_total": "4500.00",
"discount": "0.00",
"tax_rate": "20.00",
"tax_category": "standard",
"tax_scheme": "EU_VAT",
"tax_amount": "900.00",
"accounting_unit_price": "4500.00",
"accounting_line_total": "4500.00",
"period_start": "2026-04-01T00:00:00.000Z",
"period_end": "2026-06-30T23:59:59.000Z",
"accounting_classification": {
"status": "ready",
"wellCoaVersion": "1.0.0",
"intent": {
"semanticRole": "operating_expense",
"postingKind": "invoice_accrual",
"documentPolarity": "purchase",
"wellCoaVersion": "1.0.0"
},
"rawFacts": {
"well_semantic_role": "operating_expense",
"posting_kind": "invoice_accrual",
"document_polarity": "purchase",
"tax_behavior": "exclusive",
"confidence": 0.94,
"classifier_model": "claude-opus-4-6"
}
},
"created_at": "2026-04-30T09:14:22.000Z",
"updated_at": "2026-05-02T11:07:55.000Z",
"deleted_at": null
},
"relationships": {
"invoice": {
"data": { "type": "invoice", "id": "d8e1f4a2-3c7b-4b85-9e2d-6a0f7c1e4821" }
},
"ledger_account": {
"data": { "type": "ledger_account", "id": "f1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d" }
},
"applied_tax_rate": {
"data": { "type": "tax_rate", "id": "b9c8d7e6-f5a4-3b2c-1d0e-9f8e7d6c5b4a" }
},
"media": {
"data": null
}
}
}
}apps/api/src/database/entities/InvoiceItem.ts · domain: financial-graph · tier: Supporting