← All @molecule/* packages · App templates

@molecule/api-resource-invoice

API resource · resource-invoice · API (Node) · v1.0.1 · Apache-2.0

Line-item invoicing with payment tracking

npm install @molecule/api-resource-invoice

npm · Source on GitHub

How it works

@molecule/api-resource-invoice is an API resource: the routes, validation and storage for resource-invoice, built on the database and auth cores so it runs on whichever providers your app has bonded.

import { createInvoiceRouter } from '@molecule/api-resource-invoice'
app.use('/invoices', createInvoiceRouter())

Works with: @molecule/api-bonds-default-express, @molecule/api-database, @molecule/api-i18n, @molecule/api-middleware-validation

Reference

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.ts JSDoc, 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 paid

Type

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/express

API

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(): Router

deleteInvoiceForUser(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): Invoice

updateInvoiceForUser(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.1
  • express ^5.0.0
  • zod ^4.0.0

Runtime Dependencies

  • @molecule/api-bonds-default-express
  • @molecule/api-database
  • @molecule/api-i18n
  • @molecule/api-middleware-validation
  • express
  • zod

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 computeTotals down 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 to sent; a recordPayment for LESS than the balance flips it to partial; paying the remaining balance flips it to paid and 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 shows void, and once an invoice is paid or void the 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-paid or void invoice, 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.