@happyvertical/smrt-commerce
Complete commerce system with contracts, invoicing, payments, fulfillment tracking, and accounting integration.
v0.19.0InvoicingPayments6 Components
Overview
smrt-commerce provides complete commerce management including orders, invoices, payments, and fulfillment tracking. It integrates with smrt-ledgers for double-entry accounting and supports external accounting systems via the @happyvertical/accounting SDK.
Key Features:
- Contract management with STI (orders, estimates, leases, agreements)
- Invoice generation with auto-numbering and multi-status workflow
- Payment recording with allocation to invoices
- Ledger integration (AR/revenue recognition)
- Fulfillment tracking with shipping/delivery
- 6 new Svelte 5 invoice components
Installation
bash
npm install @happyvertical/smrt-commerceQuick Start
typescript
import {
CustomerCollection, InvoiceCollection,
InvoiceLineItemCollection, PaymentCollection
} from '@happyvertical/smrt-commerce';
// Initialize
const customers = await CustomerCollection.create({ db: {...} });
const invoices = await InvoiceCollection.create({ db: {...} });
const lineItems = await InvoiceLineItemCollection.create({ db: {...} });
const payments = await PaymentCollection.create({ db: {...} });
// Create customer
const customer = await customers.create({
profileId: 'profile-uuid',
creditLimit: 50000,
paymentTerms: 'Net 30'
});
await customer.save();
// Create invoice
const invoice = await invoices.create({
customerId: customer.id,
invoiceNumber: await invoices.generateInvoiceNumber(), // INV-2025-0001
issueDate: new Date(),
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
subtotal: 1000,
taxAmount: 50,
totalAmount: 1050,
status: 'DRAFT'
});
await invoice.save();
// Add line items
const item = await lineItems.create({
invoiceId: invoice.id,
description: 'Consulting Services',
quantity: 50,
unitPrice: 20,
taxRate: 0.05
});
item.amount = item.calculateAmount();
await item.save();
// Send invoice
invoice.markSent();
await invoice.save();
// Record payment
const payment = await payments.create({
customerId: customer.id,
amount: 1050,
method: 'BANK_TRANSFER'
});
await payment.save();Core Models
Contract (STI Base)
typescript
class Contract extends SmrtObject {
contractType: 'ORDER' | 'ESTIMATE' | 'LEASE' | 'AGREEMENT' | 'PURCHASE_ORDER'
status: 'DRAFT' | 'SENT' | 'ACCEPTED' | 'DECLINED' | 'COMPLETED' | 'CANCELLED'
customerId?: string
vendorId?: string
subtotal: number
taxAmount: number
totalAmount: number
issueDate: Date
dueDate?: Date
expiryDate?: Date // For estimates
reference?: string
terms?: string
isDraft(): boolean
isAccepted(): boolean
isCompleted(): boolean
isExpired(): boolean
isOverdue(): boolean
recalculateTotals(): void
}Invoice
typescript
class Invoice extends SmrtObject {
invoiceNumber: string
customerId: string
contractId?: string
issueDate: Date
dueDate: Date
paidDate?: Date
status: 'DRAFT' | 'SENT' | 'VIEWED' | 'PARTIAL' | 'PAID' | 'OVERDUE' | 'CANCELLED' | 'WRITTEN_OFF'
subtotal: number
taxAmount: number
totalAmount: number
amountPaid: number
arJournalId?: string // Ledger integration
revenueJournalId?: string
sentAt?: Date
viewedAt?: Date
// Status management
markSent(): void
markViewed(): void
updatePaymentStatus(amountPaid: number): void
cancel(): void
writeOff(): void
// Financial
getAmountDue(): number
isPaid(): boolean
isOverdue(): boolean
// Accounting integration
async recognizeRevenue(options): Promise<Journal>
toAccountingInput(): any
}Payment
typescript
class Payment extends SmrtObject {
contractId: string
customerId: string
amount: number
currency: string
method: 'CASH' | 'CHECK' | 'CREDIT_CARD' | 'BANK_TRANSFER' | 'CRYPTO' | 'OTHER'
status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'REFUNDED' | 'CANCELLED'
transactionId?: string
journalId?: string // Ledger integration
paidAt?: Date
async recordPayment(options): Promise<Journal>
markFailed(reason: string): void
cancel(): void
}Invoice Management
Invoice Lifecycle
typescript
// 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
typescript
// 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 PAIDSvelte Components (NEW v0.19.0)
Component Registration
typescript
import '@happyvertical/smrt-commerce/svelte';
// Auto-registers all 6 invoice componentsInvoiceCard
svelte
<InvoiceCard
invoice={{'
invoiceNumber: 'INV-2025-0001',
status: 'SENT',
totalAmount: 1050,
dueDate: new Date('2025-02-15'),
customerName: 'Acme Corp'
'}}
currency="USD"
href="/invoices/123"
/>InvoiceLineItems
svelte
<InvoiceLineItems
items={[
{
description: 'Consulting',
quantity: 40,
unitPrice: 125,
amount: 5000,
sourceType: 'time'
}
]}
editable={true}
currency="USD"
showSource={true}
onupdate={(item) => saveLineItem(item)}
ondelete={(item) => deleteLineItem(item)}
onadd={() => addLineItem()}
/>InvoiceActions
svelte
<InvoiceActions
status="DRAFT"
onsend={() => sendInvoice()}
onedit={() => editInvoice()}
ondelete={() => deleteInvoice()}
onprint={() => printInvoice()}
/>Best 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