@happyvertical/smrt-commerce
E-commerce with Contract STI hierarchy (5 types), invoice lifecycle, payment tracking, fulfillment, and optional ledger integration.
Overview
smrt-commerce covers customers, vendors, contracts (5 STI types), invoices with ledger integration, payments, and fulfillment tracking. Customer and Vendor link to smrt-profiles via plain string IDs. Invoice and Payment optionally integrate with smrt-ledgers via dynamic import.
Key Features:
- Customer/Vendor linked to Profile via string ID (creditLimit, paymentTerms, leadTimeDays)
- Contract STI hierarchy: Estimate, Order, Lease, Agreement, PurchaseOrder
- Invoice lifecycle: DRAFT, SENT, VIEWED, PARTIAL, PAID, OVERDUE, CANCELLED, WRITTEN_OFF
- Revenue recognition via
recognizeRevenue()(DR: AR, CR: Revenue, CR: Tax Payable) - Payment allocation to invoices with status tracking
- Fulfillment/FulfillmentLineItem for shipment/delivery tracking
- Optional ledger integration via dynamic import (returns null if not installed)
Installation
npm install @happyvertical/smrt-commerceQuick Start
import {
Customer, CustomerCollection,
Order, ContractCollection,
Invoice, InvoiceCollection,
Payment, PaymentCollection,
ContractStatus, InvoiceStatus, PaymentMethod
} from '@happyvertical/smrt-commerce';
// Create a customer linked to a profile
const customers = new CustomerCollection(db);
const customer = await customers.create({
profileId: 'profile-uuid',
creditLimit: 10000.00,
paymentTerms: 'Net 30',
});
await customer.save();
// Create an order (STI contract type)
const contracts = new ContractCollection(db);
const order = await contracts.create({
_meta_type: 'Order',
customerId: customer.id,
subtotal: 1000.00,
taxAmount: 50.00,
totalAmount: 1050.00,
currency: 'CAD',
});
await order.save();
// Create an invoice for the order
const invoices = new InvoiceCollection(db);
const invoiceNumber = await invoices.generateInvoiceNumber();
const invoice = await invoices.create({
customerId: customer.id,
contractId: order.id,
invoiceNumber,
subtotal: 1000.00,
taxAmount: 50.00,
totalAmount: 1050.00,
});
await invoice.save();
// Recognize revenue (creates balanced journal in smrt-ledgers)
await invoice.recognizeRevenue({
arAccountId: 'ar-account-id',
revenueAccountId: 'revenue-account-id',
taxAccountId: 'tax-account-id',
});
// Record a payment
const payments = new PaymentCollection(db);
const payment = await payments.create({
contractId: order.id,
customerId: customer.id,
amount: 1050.00,
method: PaymentMethod.CREDIT_CARD,
});
await payment.save();Core Models
Contract (STI Base -- 5 Types)
Contract is an STI base class. Five concrete types share one table: Estimate, Order, Lease, Agreement, and PurchaseOrder.
// STI types: Estimate, Order, Lease, Agreement, PurchaseOrder
class Contract extends SmrtObject {
// Create via ContractCollection with _meta_type
customerId?: string // FK within package
vendorId?: string
subtotal: number // decimal (not integer cents)
taxAmount: number
totalAmount: number
currency: string
status: 'draft' | 'sent' | 'accepted' | 'declined' | 'completed' | 'cancelled'
issueDate: Date
dueDate?: Date
reference?: string
terms?: string
}Invoice
Status machine: DRAFT -> SENT -> VIEWED -> PARTIAL -> PAID (also OVERDUE, CANCELLED, WRITTEN_OFF). recognizeRevenue() creates balanced AR journal entry. Ledger integration is optional via dynamic import.
class Invoice extends SmrtObject {
invoiceNumber: string
customerId: string
contractId?: string
subtotal: number // decimal (not integer cents)
taxAmount: number
totalAmount: number
amountPaid: number
status: 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'cancelled' | 'written_off'
arJournalId?: string // Plain string ref to smrt-ledgers
revenueJournalId?: string // Plain string ref to smrt-ledgers
// Status management (Invoice controls payment status, not Payment model)
markSent(): void
markViewed(): void
updatePaymentStatus(amountPaid: number): void
// Accounting integration (optional -- returns null if smrt-ledgers not installed)
async recognizeRevenue(options: RecognizeRevenueOptions): Promise<Journal | null>
// Creates: DR Accounts Receivable, CR Revenue, CR Tax Payable
}Payment / PaymentAllocation
Payment tracks payments against invoices. PaymentAllocation handles payment-to-invoice allocation. Note: Invoice controls payment status, not the Payment model -- use Invoice.updatePaymentStatus().
class Payment extends SmrtObject {
contractId: string
customerId: string
amount: number // decimal
currency: string
method: 'cash' | 'check' | 'credit_card' | 'bank_transfer' | 'crypto' | 'other'
status: 'pending' | 'completed' | 'failed' | 'refunded' | 'cancelled'
journalId?: string // Plain string ref to smrt-ledgers
// Ledger integration (optional)
async recordPayment(options: RecordPaymentOptions): Promise<Journal | null>
// Creates: DR Cash, CR Accounts Receivable
}Invoice Management
Invoice Lifecycle
// 1. Create draft invoice
const invoice = await invoices.create({
customerId: customer.id,
invoiceNumber: await invoices.generateInvoiceNumber({
prefix: 'INV', // Custom prefix
format: 'prefix-year-seq' // INV-2025-0001
}),
issueDate: new Date(),
dueDate: new Date(Date.now() + 30 * 86400000),
status: 'DRAFT'
});
await invoice.save();
// 2. Add line items
const item = await lineItems.create({
invoiceId: invoice.id,
description: 'Web Development',
quantity: 40,
unitPrice: 125,
taxRate: 0.05,
sourceType: 'contract',
sourceId: order.id
});
item.amount = item.calculateAmount();
await item.save();
// 3. Update invoice totals
invoice.subtotal = await lineItems.getSubtotalForInvoice(invoice.id);
invoice.taxAmount = await lineItems.getTaxForInvoice(invoice.id);
invoice.totalAmount = await lineItems.getTotalForInvoice(invoice.id);
await invoice.save();
// 4. Send to customer
invoice.markSent();
await invoice.save();
// 5. Track customer view
invoice.markViewed();
await invoice.save();
// 6. Recognize revenue (accounting)
const journal = await invoice.recognizeRevenue({
ledgerId: ledger.id,
receivablesAccountId: arAccount.id,
revenueAccountId: revenueAccount.id,
taxPayableAccountId: taxAccount.id
});
// Creates: Debit AR, Credit Revenue, Credit Tax PayablePayment Allocation
// Record payment
const payment = await payments.create({
contractId: order.id,
customerId: customer.id,
amount: 5250,
method: 'BANK_TRANSFER',
transactionId: 'bank_123'
});
await payment.save();
// Record with ledger
const journal = await payment.recordPayment({
ledgerId: ledger.id,
receivablesAccountId: arAccount.id,
cashAccountId: bankAccount.id
});
// Creates: Debit Cash, Credit AR
// Check available amount
const available = await allocations.getUnallocatedFromPayment(
payment.id, payment.amount
);
// Allocate to invoices
const allocation1 = await allocations.create({
paymentId: payment.id,
invoiceId: invoice1.id,
amount: 1050,
allocatedBy: 'user-uuid'
});
await allocation1.save();
const allocation2 = await allocations.create({
paymentId: payment.id,
invoiceId: invoice2.id,
amount: 4200,
allocatedBy: 'user-uuid'
});
await allocation2.save();
// Update invoice statuses
const total1 = await allocations.getTotalAllocatedToInvoice(invoice1.id);
invoice1.updatePaymentStatus(total1);
await invoice1.save(); // Status becomes PAID
const total2 = await allocations.getTotalAllocatedToInvoice(invoice2.id);
invoice2.updatePaymentStatus(total2);
await invoice2.save(); // Status becomes PARTIAL or PAIDBest Practices
DOs
- Use generateInvoiceNumber() for consistent numbering
- Always allocate payments to specific invoices
- Check getUnallocatedFromPayment() before allocating
- Call recognizeRevenue() after sending invoices
- Recalculate invoice totals after line item changes
DON'Ts
- Don't manually set invoice numbers (race conditions)
- Don't over-allocate payments (check available first)
- Don't cancel paid invoices (use writeOff if needed)
- Don't skip updatePaymentStatus() after allocation
- Don't modify line items without recalculating totals