s-m-r-t

@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-commerce

Quick 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 Payable

Payment 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 PAID

Svelte Components (NEW v0.19.0)

Component Registration

typescript
import '@happyvertical/smrt-commerce/svelte';
// Auto-registers all 6 invoice components

InvoiceCard

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

Related Modules