@molecule/api-resource-invoice
v1.0.1
Published
Invoice CRUD + line items + draft/sent/paid/overdue status. Extracted from invoice-billing flagship.
Maintainers
Readme
@molecule/api-resource-invoice
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
@molecule/api-resource-invoice — line-item-based invoice CRUD with
auto-computed totals (subtotal + tax), draft/sent/partial/paid/overdue/void
status, and a recordPayment(invoiceId, userId, amount) helper that
advances status automatically.
Extracted from the invoice-billing flagship.
Quick Start
import { createInvoiceRouter } from '@molecule/api-resource-invoice'
app.use('/invoices', createInvoiceRouter())import { createInvoiceForUser, recordPayment } from '@molecule/api-resource-invoice'
const inv = await createInvoiceForUser(userId, {
client_id: 'acme-co',
items: [{ description: 'Consulting', quantity: 10, unit_price: 250 }],
tax_rate: 8.5,
})
await recordPayment(inv.id, userId, 2710.0) // marks paidType
resource
Installation
npm install @molecule/api-resource-invoice @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-middleware-validation express zod
npm install -D @types/expressAPI
Interfaces
Invoice
Normalized invoice with all date fields serialized to ISO strings for API responses.
interface Invoice extends Omit<
InvoiceRow,
'issue_date' | 'due_date' | 'paid_at' | 'created_at' | 'updated_at'
> {
issue_date: string
due_date: string | null
paid_at: string | null
created_at: string
updated_at: string
}InvoiceRow
Raw database row shape for an invoice, with date fields typed as string or Date.
interface InvoiceRow {
id: string
user_id: string
client_id: string
number: string
status: InvoiceStatus
items: LineItem[]
subtotal: number
tax_rate: number
tax_amount: number
total: number
amount_paid: number
currency: string
issue_date: string | Date
due_date: string | Date | null
paid_at: string | Date | null
notes: string | null
created_at: string | Date
updated_at: string | Date
}LineItem
A single billable line on an invoice, with description, quantity, and unit price.
interface LineItem {
description: string
quantity: number
unit_price: number
}Types
InvoiceStatus
Lifecycle states an invoice can be in, from initial draft through payment or cancellation.
type InvoiceStatus = 'draft' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void'Functions
computeTotals(items, taxRate)
Compute subtotal/tax/total from line items + tax rate (percent 0-100).
function computeTotals(
items: LineItem[],
taxRate: number,
): { subtotal: number; tax_amount: number; total: number }createInvoiceForUser(userId, data)
Create a new draft invoice for a user, computing totals from line items and tax rate.
function createInvoiceForUser(
userId: string,
data: {
client_id: string
items: LineItem[]
due_date?: string
notes?: string
tax_rate?: number
currency?: string
},
): Promise<Invoice>createInvoiceRouter()
Creates and returns an Express router with all CRUD + payment routes for invoices.
function createInvoiceRouter(): RouterdeleteInvoiceForUser(invoiceId, userId)
Delete a user-owned invoice by ID, returning false if not found or not owned by the user.
function deleteInvoiceForUser(invoiceId: string, userId: string): Promise<boolean>getInvoiceForUser(invoiceId, userId)
Fetch a single invoice by ID, returning null if not found or not owned by the user.
function getInvoiceForUser(invoiceId: string, userId: string): Promise<Invoice | null>listInvoicesForUser(userId, opts?)
List all invoices for a user, with optional client/status filters and pagination.
function listInvoicesForUser(
userId: string,
opts?: { client_id?: string; status?: InvoiceStatus; page?: number; limit?: number },
): Promise<{ data: Invoice[]; total: number; page: number; limit: number }>recordPayment(invoiceId, userId, amount)
Record a payment amount against an invoice, updating amount_paid and transitioning status to partial or paid.
function recordPayment(invoiceId: string, userId: string, amount: number): Promise<Invoice | null>toInvoice(row)
Map a raw database InvoiceRow to a normalized Invoice with ISO date strings.
function toInvoice(row: InvoiceRow): InvoiceupdateInvoiceForUser(invoiceId, userId, patch)
Apply a partial update to a user-owned invoice, recomputing totals and marking paid_at when status becomes paid.
function updateInvoiceForUser(
invoiceId: string,
userId: string,
patch: Partial<{
items: LineItem[]
due_date: string
notes: string
tax_rate: number
currency: string
status: InvoiceStatus
}>,
): Promise<Invoice | null>Constants
INVOICE_STATUSES
All valid lifecycle statuses an invoice can hold.
const INVOICE_STATUSES: readonly ['draft', 'sent', 'partial', 'paid', 'overdue', 'void']invoiceCreateSchema
Zod schema for creating a new invoice (client, items, optional due date / notes / tax / currency).
const invoiceCreateSchema: z.ZodObject<
{
client_id: z.ZodString
items: z.ZodArray<
z.ZodObject<
{ description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
z.core.$strip
>
>
due_date: z.ZodOptional<z.ZodString>
notes: z.ZodOptional<z.ZodString>
tax_rate: z.ZodOptional<z.ZodNumber>
currency: z.ZodOptional<z.ZodString>
},
z.core.$strip
>invoiceUpdateSchema
Zod schema for partially updating an existing invoice (all fields optional).
const invoiceUpdateSchema: z.ZodObject<
{
items: z.ZodOptional<
z.ZodArray<
z.ZodObject<
{ description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
z.core.$strip
>
>
>
due_date: z.ZodOptional<z.ZodString>
notes: z.ZodOptional<z.ZodString>
tax_rate: z.ZodOptional<z.ZodNumber>
currency: z.ZodOptional<z.ZodString>
status: z.ZodOptional<
z.ZodEnum<{
draft: 'draft'
sent: 'sent'
partial: 'partial'
paid: 'paid'
overdue: 'overdue'
void: 'void'
}>
>
},
z.core.$strip
>lineItemSchema
Zod schema for a single invoice line item (description, quantity, unit price).
const lineItemSchema: z.ZodObject<
{ description: z.ZodString; quantity: z.ZodNumber; unit_price: z.ZodNumber },
z.core.$strip
>recordPaymentSchema
Zod schema for recording a payment against an invoice (positive amount required).
const recordPaymentSchema: z.ZodObject<{ amount: z.ZodNumber }, z.core.$strip>Injection Notes
Requirements
Peer dependencies:
@molecule/api-bonds-default-express^1.0.1@molecule/api-database^1.0.1@molecule/api-i18n^1.0.1@molecule/api-middleware-validation^1.0.1express^5.0.0zod^4.0.0
Runtime Dependencies
@molecule/api-bonds-default-express@molecule/api-database@molecule/api-i18n@molecule/api-middleware-validationexpresszod
Table: src/__setup__/invoices.sql creates the single invoices table. An
mlcl-scaffolded API replays __setup__/*.sql automatically on migrate;
anywhere else run it once. The bundled computeTotals(items, taxRate)
helper is exported for client-side total previews.
Everything is owner-scoped: service functions take the authenticated
userId and return null/false for rows the caller doesn't own, and the
router reads the caller from res.locals.session (mount behind your global
auth middleware; 401 otherwise). recordPayment is bookkeeping — "record a
payment I received" against my own invoice — it never talks to a payment
provider; wire actual charging separately (see @molecule/api-payments).
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- [ ] Creating an invoice with line items auto-computes the money: add a
line (quantity × unit_price) and the subtotal updates, the tax_rate is
applied (tax_amount = subtotal × rate / 100), and total = subtotal +
tax_amount — matching
computeTotalsdown to the rounded cents shown in the UI. Change a quantity or add another line and every figure re-computes. - [ ] The status lifecycle advances correctly through INVOICE_STATUSES: a
freshly created invoice reads
draft; sending it moves it tosent; arecordPaymentfor LESS than the balance flips it topartial; paying the remaining balance flips it topaidand stamps a paid date. The balance-due shown to the user (total - amount_paid) is correct after each step. - [ ] Terminal/edge states read right: an unpaid invoice past its due_date
shows
overdue, a cancelled one showsvoid, and once an invoice ispaidorvoidthe UI blocks further line-item/total edits as designed. - [ ] Overpayment and dead-invoice payments are rejected, not silently
over-applied: recording an amount greater than the outstanding balance, or
any payment against an already-
paidorvoidinvoice, fails with a visible error and leaves amount_paid + status unchanged. - [ ] AUTHORIZATION: a signed-in user sees and mutates only THEIR OWN
invoices — every path is
*ForUser-scoped (listInvoicesForUser / getInvoiceForUser / updateInvoiceForUser / recordPayment). Guessing or tampering another user's invoice id on GET/PUT/DELETE/:id/payment returns 403/404 — never that invoice's data and never a cross-user payment.
