@happyvertical/smrt-commerce
Customers, vendors, contracts, invoices, credit notes, payments, payment rails, tax codes, and fulfillment, with four recipes.
What it gives you
Customers, vendors, contracts, invoices, credit notes, payments, payment rails, tax codes, and fulfillment, with four recipes.
- 01Four recipes: customers, vendors, sales, and purchases
- 02Tenant tax codes with exclusive, inclusive, or no-tax line pricing over exact integer minor units
- 03Issued invoices keep their net amounts and stored tax; credit notes and the tax summary read those facts
- 04One issuing provider and any number of payment rails
Recipes cover the headers first
commerce.customers, commerce.vendors, commerce.sales, and commerce.purchases are declared in the package and emitted into the manifest recipes. Sales requires customers and purchases requires vendors. They cover Customer, Vendor, Order, and PurchaseOrder headers only; line items, fulfillment, invoices, and payments are later recipes. Each ships user-facing help, and the fields it shows carry plain-language descriptions.
One contract hierarchy supports commercial lifecycle
channelId distinguishes Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, and LicenseSale in one table. LicenseSale snapshots rights immutably and revoke() is its explicit transition. Money is stored as integer minor units. InvoiceLineItem.quantity is decimal with six fractional places; rounding happens once, where quantity times price meets money. There are no tolerances on any money path. The cost-billing subpath is an independent entry for signed cost billing with explicit tax rounding.
Sales and purchase contracts have separate parties
The base Contract holds no party. Abstract SalesContract owns customerId and covers Order, Estimate, Agreement, Lease, LicenseSale, WholesaleOrder, and Cart. Abstract PurchaseContract owns vendorId and covers PurchaseOrder and ProductionOrder. A sales leaf refuses vendorId and a purchase leaf refuses customerId. The contractId on Invoice, Payment, and Fulfillment points at SalesContract. Run preflightCommercePartyLinks on existing data before an upgrade.
Tax codes and components
A TaxCode lists one or more TaxComponent rows. Each has an exact percent and an optional liability account; a GST plus PST code has two. A used component never changes its percent: archive it and add a replacement. Every tenant starts with exempt, zero-rated, and out-of-scope codes. TaxSettings holds the tenant defaults, registrations, line amount type, and rounding scope. A line resolves its code from its own override, then a customer exemption, the product, the customer, and the tenant default. applyTaxCodeExamplePack imports example codes for Canada, the UK, Australia, and New Zealand; they are examples to review, not jurisdiction rules.
Exclusive, inclusive, or no-tax pricing
calculateMoneyDocument prices each line as quantity times unit price less one discount. An exclusive line amount is the net before tax. An inclusive amount is the gross the customer sees, and the net is derived by removing tax. A none line carries no tax and needs an out-of-scope code. Rounding is per line or per document, and the chosen scope is stored with the invoice. previewInvoiceMoney shows a proposed draft without writing it.
Issued invoices keep net amounts and stored tax
Saving a draft line recalculates the whole invoice once and writes the header totals, each line, and its DocumentTax rows together. InvoiceLineItem.amount is the net line amount and taxAmount is stored beside it. DocumentTax is read-only and records the code, component, percent, liability account, taxable base, and tax for each document. When an invoice leaves draft its money is frozen, and later edits to amounts or lines are refused. getInvoiceMoneyFacts reads the stored facts without recalculating them, and InvoiceRecordScreen shows them on a record page.
Every invoice and contract write needs a tenant
After creating a tenant, call ensureCommerceTenant(db, tenantId, { cookbook }) once. It seeds the tax settings, the standard tax codes, and the cookbook defaults, returns { tenantId }, and is safe to repeat. Money data written by an earlier release is refused with COMMERCE_DATA_NEEDS_UPGRADE, and the error names the one-time command to run: smrt db:upgrade-commerce-money.
Credit notes
A credit note is an Invoice with kind credit_note that names the invoice it credits. It credits a quantity or an amount of a source line, never more than the line, or adds a goodwill line with its own tax code. Tax comes from the source invoice as issued, not from current settings. An issued credit note reduces the source balance. A credit refund is a Payment of kind refund, allocated only to a credit note. Refunding an unapplied customer advance is a separate command, refundCustomerFunds, and that refund can never be allocated to an invoice credit.
Posting by account and the tax summary
Revenue recognition posts each line net to the revenue account stored on the line. Each tax component posts to its own liability account. A credit note posts the reverse. cancelWithRevenueReversal cancels a posted invoice by posting an exact reversing journal. TaxSummaryReport is a materialized report of taxable amounts, tax, and recoverable tax by period, currency, kind of tax code, and component. Expense and ledger figures sit beside them, and each figure drills down to its documents. createTaxSummaryMaterializedReportHost registers it with an application, and MaterializedReportTable from smrt-reports renders it.
Invoices, payments, and payouts retain evidence
recognizeRevenue posts accounts-receivable journal evidence. PaymentAllocation and Invoice.updatePaymentStatus connect settlement to the invoice. PaymentIntent locks a multi-rail price behind an idempotency key, while PaymentInstrument identifies the selected rail. Vendor payouts move from pending to sent, confirmed, or failed.
Payment rails behind one issuing provider
A runtime has one issuing provider and any number of paymentProviders, each with a unique name and its own inbox namespace. Settlement is decided by the pure decideAttempt from gateway state and the payment policy; the gateway is authoritative, and nothing here counts confirmations. Crypto rails implement the port over the payments package and never call a gateway from here. Rails never save cards or charge tax. CustomerSelect and VendorSelect are the registered selectors; QuoteEditor and PurchaseOrderEditor keep draft strings apart from minor-unit revisions.
Package UI
Components in this package
Import these public components directly. When the package also exports playground entries, the next tab renders its real components with package-owned example data.
Package previews
Try the working views
These previews are owned by the package and use its public components. Mock mode is intended for safe browsing; live mode appears only where the package supports it.
Generated interface
REST
The exact route set comes from each model's @smrt({ api }) policy
and the application manifest—not from a second controller definition.
GET /api/invoices
GET /api/invoices/:id
POST /api/invoices
PATCH /api/invoices/:idWhat stays consistent
- Tenant scope and operation permissions run before model access.
- OpenAPI and generated clients use the same manifest fields.
- Packages can expose read-only or custom action sets.
Generated interface
MCP
AI tools are generated from the methods you explicitly include. Their inputs are validated against the same model metadata used by REST and CLI.
invoices_list({ where, limit, orderBy })
invoices_get({ id })
invoices_create({ data })
invoices_update({ id, data })Agent-safe by construction
- Tool availability can be narrower than the HTTP API.
- Principal execution intersects RBAC, agent ceilings, and persona tools.
- Descriptions and schemas are emitted from source metadata.
Browser-native agent interface
WebMCP
The generated web definitions carry the same tool names, descriptions, and JSON Schema
inputs as MCP. smrt-web registers them with document.modelContext.
import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
import { registerWebMcpTools } from '@happyvertical/smrt-web';
// Omitting an exposure policy registers read-only tools only.
const dispose = registerWebMcpTools(webMcpToolDefinitions);The page keeps its security boundary
- Tools execute through generated REST fetchers as the signed-in page user.
- Authentication, tenant gates, permissions, and field policy stay server-side.
- Feature detection makes registration a safe no-op outside supported browsers.
Generated interface
CLI
The app CLI turns manifest resources into predictable operator commands while keeping package-specific actions close to their models.
smrt invoices list --limit 25 --order-by created_at:desc
smrt invoices get <id>
smrt invoices create --from-file ./record.jsonMade for automation
- Machine-readable output and file-based input.
- The same include/exclude policy as other surfaces.
- Migration, introspection, dispatch, and knowledge commands stay in smrt-cli.