s-m-r-t

@happyvertical/smrt-ledgers

Double-entry accounting with chart of accounts, journals, and financial reporting.

v0.19.0AccountingDouble-Entry

Overview

smrt-ledgers is a production-ready double-entry accounting system built on the SMRT framework. It provides the core accounting engine for financial systems, commerce modules, and business applications that need to track monetary transactions with proper accounting principles.

The module enforces double-entry bookkeeping rules (debits = credits), supports hierarchical chart of accounts, multi-currency transactions, and tracks journal lifecycle from draft to posted to voided. Running balances and trial balances are calculated automatically from posted entries.

Installation

bash
npm install @happyvertical/smrt-ledgers
# or
pnpm add @happyvertical/smrt-ledgers

The module depends on @happyvertical/smrt-core for SmrtObject and SmrtCollection base classes. It integrates seamlessly with smrt-tenancy for multi-tenant isolation and smrt-commerce for automatic transaction recording.

Quick Start (5 Minutes)

Here's a minimal example showing how to set up a chart of accounts, create a journal entry, and post a transaction:

1. Initialize Collections

typescript
import { AccountCollection, JournalCollection, JournalEntryCollection } from '@happyvertical/smrt-ledgers';

const accounts = new AccountCollection({ db: {...} });
const journals = new JournalCollection({ db: {...} });
const entries = new JournalEntryCollection({ db: {...} });

2. Create Chart of Accounts

typescript
// Create asset account (debit-normal)
const cashAccount = await accounts.create({
  number: '1000',
  name: 'Cash',
  type: 'asset',
  description: 'Cash on hand and in bank'
});

// Create revenue account (credit-normal)
const salesAccount = await accounts.create({
  number: '4000',
  name: 'Sales Revenue',
  type: 'revenue',
  description: 'Product and service sales'
});

3. Create and Post a Journal Entry

typescript
// Record a $150 cash sale
const journal = await journals.createWithEntries({
  date: new Date('2025-01-12'),
  description: 'Cash sale - Product #123',
  entries: [
    {
      accountId: cashAccount.id,
      debit: 150,
      credit: 0,
      currency: 'USD',
      memo: 'Cash received'
    },
    {
      accountId: salesAccount.id,
      debit: 0,
      credit: 150,
      currency: 'USD',
      memo: 'Revenue recognized'
    }
  ]
});

// Post the journal (validates balance first)
await journal.post();
console.log('Journal posted:', journal.number); // "JNL-1736640000000-abc123"

4. Query Account Balances

typescript
// Get current balance for cash account
const cashBalance = await entries.getAccountBalance(cashAccount.id);
console.log('Cash balance:', cashBalance); // 150 (debit-normal, so positive)

// Get trial balance for all accounts
const trialBalance = await entries.getTrialBalance();
console.log('Trial balance:', trialBalance);
// [
//   { accountNumber: '1000', accountName: 'Cash', debit: 150, credit: 0 },
//   { accountNumber: '4000', accountName: 'Sales Revenue', debit: 0, credit: 150 }
// ]
// Total debits: 150, Total credits: 150 ✓

Core Concepts

Double-Entry Bookkeeping

Every transaction must balance: Total Debits = Total Credits. The system enforces this rule before allowing journals to be posted. A floating-point tolerance (BALANCE_EPSILON = 0.001) is used for comparisons.

Account Types

The five fundamental account types follow standard accounting principles. Each type has a "normal balance" (debit or credit) that determines how increases and decreases are recorded:

1. Asset Accounts (Debit-Normal)

Resources owned by the business. Increased by debits, decreased by credits.

Examples: Cash, Bank Accounts, Accounts Receivable, Inventory, Equipment

Balance = Debits - Credits

2. Liability Accounts (Credit-Normal)

Obligations owed to others. Increased by credits, decreased by debits.

Examples: Accounts Payable, Credit Cards, Loans, Accrued Expenses

Balance = Credits - Debits

3. Equity Accounts (Credit-Normal)

Owner's stake in the business. Increased by credits, decreased by debits.

Examples: Owner Capital, Retained Earnings, Common Stock

Balance = Credits - Debits

4. Revenue Accounts (Credit-Normal)

Income earned from operations. Increased by credits, decreased by debits.

Examples: Sales, Service Income, Interest Income, Rental Income

Balance = Credits - Debits

5. Expense Accounts (Debit-Normal)

Costs of doing business. Increased by debits, decreased by credits.

Examples: Rent, Utilities, Supplies, Cost of Goods Sold, Salaries

Balance = Debits - Credits

Hierarchical Chart of Accounts

Accounts can be organized in parent-child hierarchies for better organization and reporting:

typescript
// Create parent account
const assets = await accounts.create({
  number: '1000',
  name: 'Assets',
  type: 'asset'
});

// Create child account
const currentAssets = await assets.createChild({
  number: '1100',
  name: 'Current Assets'
});

// Create grandchild
const cash = await currentAssets.createChild({
  number: '1110',
  name: 'Cash'
});

// Get full path
console.log(await cash.getFullPath()); // "Assets > Current Assets > Cash"

Journal Lifecycle

Journals progress through three states:

Draft Status

Editable state where entries can be added or removed. Journals start in draft and must balance before posting. Draft journals do not affect account balances.

Posted Status

Immutable, locked state after validation. Posted journals are included in balance calculations. The postedAt timestamp is recorded. Cannot be edited or deleted.

Voided Status

Marked as cancelled with reason. Voided journals are excluded from balance calculations. The voidedAt timestamp and voidReason are recorded.

typescript
// Create draft journal
const journal = await journals.createWithEntries({ /* ... */ });
console.log(journal.isDraft()); // true

// Add more entries while in draft
await journal.addEntry({
  accountId: taxAccount.id,
  debit: 10,
  credit: 0,
  memo: 'Sales tax'
});

// Post when ready (validates balance)
await journal.post();
console.log(journal.isPosted()); // true
console.log(journal.isEditable()); // false

// Void if needed
await journal.void('Entry error - wrong amount');
console.log(journal.isVoided()); // true

API Reference

Account Model

typescript
class Account extends SmrtObject {
  // Core properties
  number: string              // e.g., "1000", "5030"
  name: string                // e.g., "Cash", "Coffee Expense"
  description: string
  type: AccountType           // 'asset' | 'liability' | 'equity' | 'revenue' | 'expense'
  parentId: string | null     // Parent account for hierarchy
  active: boolean
  metadata: Record<string, unknown>

  // Type-checking methods
  isDebitNormal(): boolean    // true for asset, expense
  isCreditNormal(): boolean   // true for liability, equity, revenue
  isTopLevel(): boolean

  // Hierarchy navigation
  async getParent(): Account | null
  async getChildren(): Account[]
  async getAncestors(): Account[]
  async getFullPath(): string                    // "Assets > Current > Cash"
  async toTreeNode(): AccountTreeNode
  async createChild(options): Account

  // Balance queries
  async getBalance(asOfDate?: Date): number
}

Journal Model

typescript
class Journal extends SmrtObject {
  // Core properties
  number: string              // Auto-generated JNL-{timestamp}-{random}
  date: Date
  description: string
  sourceModule: string        // e.g., "smrt-commerce", "manual"
  sourceRef: string | null    // External reference
  status: JournalStatus       // 'draft' | 'posted' | 'voided'
  postedAt: Date | null
  voidedAt: Date | null
  voidReason: string | null
  metadata: Record<string, unknown>

  // Status checks
  isDraft(): boolean
  isPosted(): boolean
  isVoided(): boolean
  isEditable(): boolean       // Only true for draft

  // Entry operations
  async getEntries(): JournalEntry[]
  async addEntry(data: JournalEntryData): void    // Draft-only

  // Balance validation
  async getTotalDebits(): number
  async getTotalCredits(): number
  async isBalanced(): boolean

  // State transitions
  async post(): void                             // Validates balance first
  async void(reason: string): void

  // Summary
  async summarize(): string                      // AI-generated summary
}

JournalEntry Model

typescript
class JournalEntry extends SmrtObject {
  // Core properties
  journalId: string           // Parent journal
  accountId: string           // Account affected
  debit: number               // Left side (0 if credit)
  credit: number              // Right side (0 if debit)
  currency: string            // e.g., "USD", "EUR"
  exchangeRate: number        // To base currency
  memo: string
  metadata: Record<string, unknown>

  // Amount checks
  isDebit(): boolean
  isCredit(): boolean
  getAmount(): number         // Positive value
  getBaseAmount(): number     // Amount * exchangeRate

  // Navigation
  async getJournal(): Journal | null
  async getAccount(): Account | null
  async getDescription(): string                // "DR Cash: $100 - Sales"
}

AccountCollection Methods

typescript
// Find accounts
await accounts.findByNumber('1000')
await accounts.findByType('asset')
await accounts.findActive()
await accounts.findTopLevel()
await accounts.findChildren(parentId)

// Organization
await accounts.getTree()                    // Full tree structure
await accounts.groupByType()                // Organized by type
await accounts.getDescendants(accountId)    // All children recursively

// Create or retrieve
await accounts.getOrCreateByNumber('1000', {
  name: 'Cash',
  type: 'asset'
})

JournalCollection Methods

typescript
// Find journals
await journals.findByNumber('JNL-123')
await journals.findByDateRange(startDate, endDate)
await journals.findBySource('smrt-commerce')
await journals.findByStatus('posted')
await journals.findDrafts()
await journals.findPosted()
await journals.findBySourceRef(orderId)
await journals.findByMonth(2025, 1)

// Create and manage
await journals.createWithEntries({
  date: new Date(),
  description: 'Transaction',
  entries: [...]
})
await journals.post(journalId)
await journals.void(journalId, 'Reason')

JournalEntryCollection Methods

typescript
// Find entries
await entries.findByJournal(journalId)
await entries.findByAccount(accountId)
await entries.findByAccounts([id1, id2])    // Avoids N+1

// Balance calculations
await entries.getAccountBalance(accountId, asOfDate)
// Returns signed balance: Asset/Expense = Debits - Credits
//                        Liability/Equity/Revenue = Credits - Debits

await entries.getTrialBalance(asOfDate)
// Returns: Array<{
//   accountNumber: string,
//   accountName: string,
//   debit: number,
//   credit: number
// }>

await entries.getTotalsForDateRange(startDate, endDate)
// Returns: { totalDebits: number; totalCredits: number }

await entries.getAccountLedger(accountId)
// Returns: Array<{
//   entry: JournalEntry,
//   runningBalance: number
// }>

Tutorials

Tutorial 1: Basic Ledger Setup and First Transaction (10-15 min)

Learn the fundamentals:

  • Initialize account and journal collections
  • Create simple chart of accounts (Cash, Sales Revenue)
  • Create a draft journal with two entries
  • Validate balance and post the journal
  • Query account balances

Tutorial 2: Chart of Accounts and Account Management (15-20 min)

Build hierarchical account structures:

  • Create parent-child account relationships (Assets > Current > Cash)
  • Use getOrCreateByNumber for idempotent operations
  • Organize accounts by type with groupByType()
  • Display account tree structure
  • Navigate hierarchy with getFullPath()

Tutorial 3: Complex Multi-Entry Transactions (15-20 min)

Handle real-world scenarios:

  • Record product purchase with inventory tracking
  • Account for Cost of Goods Sold (COGS)
  • Handle tax entries in multi-entry journals
  • Manage draft → posted workflow
  • Void transactions with detailed reasons

Tutorial 4: Reporting and Reconciliation (15-20 min)

Generate financial reports:

  • Generate trial balance reports
  • Calculate account balances as of specific dates
  • Create account ledgers with running balances
  • Calculate totals for date ranges
  • Integrate with external source modules

Real-World Examples

Example 1: Small Business Daily Operations

Track daily transactions for a small business:

typescript
// Morning: Owner invests $5,000 cash
await journals.createWithEntries({
  date: new Date('2025-01-12 09:00'),
  description: 'Initial capital investment',
  entries: [
    { accountId: cashId, debit: 5000, credit: 0, memo: 'Owner investment' },
    { accountId: equityId, debit: 0, credit: 5000, memo: 'Owner capital' }
  ]
}).then(j => j.post());

// Purchase coffee supplies on credit card
await journals.createWithEntries({
  date: new Date('2025-01-12 10:30'),
  description: 'Coffee supplies purchase',
  entries: [
    { accountId: suppliesId, debit: 50, credit: 0, memo: 'Coffee beans' },
    { accountId: creditCardId, debit: 0, credit: 50, memo: 'Business credit card' }
  ]
}).then(j => j.post());

// Product sold for $150 cash
await journals.createWithEntries({
  date: new Date('2025-01-12 14:00'),
  description: 'Cash sale - Product #123',
  entries: [
    { accountId: cashId, debit: 150, credit: 0, memo: 'Cash received' },
    { accountId: salesId, debit: 0, credit: 150, memo: 'Revenue' }
  ]
}).then(j => j.post());

// Record COGS for inventory sold
await journals.createWithEntries({
  date: new Date('2025-01-12 14:00'),
  description: 'COGS for sale #123',
  entries: [
    { accountId: cogsId, debit: 60, credit: 0, memo: 'Cost of goods' },
    { accountId: inventoryId, debit: 0, credit: 60, memo: 'Inventory reduction' }
  ]
}).then(j => j.post());

// End-of-day balance check
const cashBalance = await entries.getAccountBalance(cashId);
console.log('Cash on hand:', cashBalance); // $5,100 (5000 + 150 - 50)

const apBalance = await entries.getAccountBalance(creditCardId);
console.log('Credit card payable:', apBalance); // $50

Example 2: SaaS Subscription Billing

Handle recurring subscription revenue:

typescript
// Set up revenue accounts
const subscriptionRevenue = await accounts.create({
  number: '4100',
  name: 'Subscription Revenue',
  type: 'revenue'
});

const accountsReceivable = await accounts.create({
  number: '1200',
  name: 'Accounts Receivable',
  type: 'asset'
});

// Bill customer for monthly subscription
const invoice = await journals.createWithEntries({
  date: new Date('2025-01-01'),
  description: 'Monthly subscription - Customer ABC',
  sourceModule: 'smrt-commerce',
  sourceRef: 'invoice-12345',
  entries: [
    {
      accountId: accountsReceivable.id,
      debit: 99,
      credit: 0,
      memo: 'Invoice #12345'
    },
    {
      accountId: subscriptionRevenue.id,
      debit: 0,
      credit: 99,
      memo: 'Monthly Pro plan'
    }
  ]
});
await invoice.post();

// When payment received
const payment = await journals.createWithEntries({
  date: new Date('2025-01-05'),
  description: 'Payment received - Customer ABC',
  sourceModule: 'smrt-commerce',
  sourceRef: 'payment-67890',
  entries: [
    { accountId: cashId, debit: 99, credit: 0, memo: 'Payment received' },
    { accountId: accountsReceivable.id, debit: 0, credit: 99, memo: 'Invoice paid' }
  ]
});
await payment.post();

// Calculate monthly recurring revenue
const revenueBalance = await entries.getAccountBalance(
  subscriptionRevenue.id,
  new Date('2025-01-31')
);
console.log('January MRR:', revenueBalance);

Example 3: Multi-Currency E-Commerce

Handle international transactions with exchange rates:

typescript
// Order received in EUR (€100) with exchange rate 1.1
const orderJournal = await journals.createWithEntries({
  date: new Date('2025-01-10'),
  description: 'International order - Customer EU',
  sourceModule: 'smrt-commerce',
  sourceRef: 'order-999',
  entries: [
    {
      accountId: accountsReceivable.id,
      debit: 100,
      credit: 0,
      currency: 'EUR',
      exchangeRate: 1.1,
      memo: 'Order in EUR'
    },
    {
      accountId: salesRevenue.id,
      debit: 0,
      credit: 100,
      currency: 'EUR',
      exchangeRate: 1.1,
      memo: 'Revenue in EUR'
    }
  ]
});
await orderJournal.post();

// Payment received in USD: $110 (100 * 1.1)
const paymentJournal = await journals.createWithEntries({
  date: new Date('2025-01-15'),
  description: 'Payment received - Customer EU',
  entries: [
    {
      accountId: cashId,
      debit: 110,
      credit: 0,
      currency: 'USD',
      exchangeRate: 1.0,
      memo: 'USD payment'
    },
    {
      accountId: accountsReceivable.id,
      debit: 0,
      credit: 110,
      currency: 'USD',
      exchangeRate: 1.0,
      memo: 'AR paid in USD (base amount = 100 EUR * 1.1)'
    }
  ]
});
await paymentJournal.post();

// Both entries have same base amount (110 USD), so they balance correctly

Example 4: Month-End Close Process

Generate reports and close the month:

typescript
// Generate trial balance for January
const janTrialBalance = await entries.getTrialBalance(
  new Date('2025-01-31')
);

console.log('Trial Balance - January 2025');
console.log('================================');
janTrialBalance.forEach(row => {
  console.log(
    row.accountNumber.padEnd(6),
    row.accountName.padEnd(30),
    'DR:', row.debit.toFixed(2).padStart(10),
    'CR:', row.credit.toFixed(2).padStart(10)
  );
});

// Verify balance
const totalDebits = janTrialBalance.reduce((sum, row) => sum + row.debit, 0);
const totalCredits = janTrialBalance.reduce((sum, row) => sum + row.credit, 0);
console.log('Total Debits:', totalDebits.toFixed(2));
console.log('Total Credits:', totalCredits.toFixed(2));
console.log('Balanced:', Math.abs(totalDebits - totalCredits) < 0.01 ? '✓' : '✗');

// Post accrual entries for month-end
await journals.createWithEntries({
  date: new Date('2025-01-31'),
  description: 'Accrued rent expense',
  entries: [
    { accountId: rentExpenseId, debit: 2000, credit: 0, memo: 'January rent' },
    { accountId: accruedExpensesId, debit: 0, credit: 2000, memo: 'Accrued rent' }
  ]
}).then(j => j.post());

// Get account ledger with running balance
const cashLedger = await entries.getAccountLedger(cashId);
console.log('Cash Ledger:');
cashLedger.forEach(({ entry, runningBalance }) => {
  console.log(
    entry.date.toISOString().split('T')[0],
    entry.memo.padEnd(30),
    entry.isDebit() ? '+' + entry.getAmount() : '-' + entry.getAmount(),
    'Balance:', runningBalance.toFixed(2)
  );
});

Example 5: Integration with Commerce Module

Automatically create journals from commerce transactions:

typescript
// In your smrt-commerce order processing
import { JournalCollection } from '@happyvertical/smrt-ledgers';

async function recordOrderSale(order: Order) {
  const journals = new JournalCollection({ db: {...} });

  // Create journal for the sale
  const saleJournal = await journals.createWithEntries({
    date: order.createdAt,
    description: 'Sale - Order #' + order.number,
    sourceModule: 'smrt-commerce',
    sourceRef: order.id,
    entries: [
      // Record revenue
      {
        accountId: cashAccountId,
        debit: order.total,
        credit: 0,
        currency: order.currency,
        memo: 'Payment received'
      },
      {
        accountId: salesRevenueId,
        debit: 0,
        credit: order.subtotal,
        currency: order.currency,
        memo: 'Product sales'
      },
      // Record tax collected
      {
        accountId: salesTaxPayableId,
        debit: 0,
        credit: order.taxAmount,
        currency: order.currency,
        memo: 'Sales tax collected'
      }
    ]
  });
  await saleJournal.post();

  // Create separate journal for COGS
  const cogsJournal = await journals.createWithEntries({
    date: order.createdAt,
    description: 'COGS - Order #' + order.number,
    sourceModule: 'smrt-commerce',
    sourceRef: order.id,
    entries: [
      {
        accountId: cogsAccountId,
        debit: order.costOfGoods,
        credit: 0,
        memo: 'Cost of goods sold'
      },
      {
        accountId: inventoryAccountId,
        debit: 0,
        credit: order.costOfGoods,
        memo: 'Inventory reduction'
      }
    ]
  });
  await cogsJournal.post();

  return { saleJournal, cogsJournal };
}

Integration Patterns

With smrt-core

All models extend SmrtObject for standard CRUD, validation, and lifecycle hooks. Collections extend SmrtCollection for querying, pagination, and advanced filtering.

  • @smrt decorator generates API endpoints, MCP tools, and CLI commands
  • Automatic schema generation from TypeScript types
  • Built-in validation and error handling
  • Lifecycle hooks: beforeSave, afterSave, beforeDelete

With smrt-commerce

Commerce orders automatically create ledger entries for sales, COGS, and inventory tracking:

  • Orders trigger journal creation via sourceModule = "smrt-commerce"
  • Automatic COGS calculation and inventory reduction
  • Revenue and tax accounts updated per order
  • Source tracking with sourceRef = orderId

With smrt-tenancy

Ledgers are automatically isolated per tenant using @TenantScoped decorator:

  • Each tenant has separate chart of accounts
  • Journals and entries filtered by tenant context
  • Shared schema, separate data per tenant
  • Tenant ID injected automatically via AsyncLocalStorage

With smrt-users

User permissions control ledger access and modifications:

  • RBAC controls who can create/post/void journals
  • Audit trail tracks createdBy and updatedBy
  • Permission checks before posting or voiding
  • User-specific ledger views based on roles

With smrt-profiles

Organization profiles link to chart of accounts:

  • Profile metadata stores company-specific account numbering
  • Multi-entity organizations have separate account trees
  • Profile-level fiscal year and currency settings

Best Practices

Account Numbering

  • Use standardized ranges (1000-1999 Assets, 2000-2999 Liabilities, etc.)
  • Include consistent leading zeros (e.g., "1000" not "1000")
  • Reserve ranges for future sub-accounts (1100-1199 for Current Assets)
  • Document your numbering scheme in organization metadata

Data Validation

  • Always create accounts before creating journal entries
  • Use getOrCreateByNumber() for idempotent account setup
  • Validate journal balance before calling post()
  • Check account active status before use
  • Verify source references exist before linking

Posting Workflow

  • Keep journals in draft while building entries
  • Validate complete transaction logic before posting
  • Post only when all entries are finalized and balanced
  • Never attempt to modify after posting (immutable)
  • Use void() with detailed reasons for corrections

Performance

  • Use findByAccounts() for bulk queries (avoids N+1)
  • Cache account tree structure for frequent access
  • Filter by date early in queries (use indexed columns)
  • Include only active accounts in reports
  • Consider archiving old journals for historical data

Multi-Currency

  • Always set exchangeRate for non-base currency entries
  • Store amounts in original currency for audit trail
  • Calculate base amount: amount * exchangeRate
  • Track realized and unrealized gains/losses separately
  • Use consistent decimal precision (2 places for most currencies)

Common Mistakes to Avoid

  • Don't create entries with both debit and credit amounts
  • Don't modify journal entries after posting (immutable)
  • Don't delete accounts with existing entries (set inactive instead)
  • Don't include draft journals in balance calculations
  • Don't forget to post journals (draft won't affect balances)
  • Don't use negative amounts (use opposite side instead)

Common Issues and Troubleshooting

Error: "Journal is not balanced"

Cause: Total debits ≠ total credits

Solution: Check all entry amounts. Ensure the sum of debits equals the sum of credits before calling post(). Use journal.getTotalDebits() and journal.getTotalCredits() to debug.

typescript
const debits = await journal.getTotalDebits();
const credits = await journal.getTotalCredits();
console.log('Debits:', debits, 'Credits:', credits);
console.log('Difference:', Math.abs(debits - credits));

Error: "Cannot add entries to posted journal"

Cause: Trying to modify a posted (immutable) journal

Solution: Posted journals cannot be edited. Void the journal with a reason and create a new correcting journal instead.

typescript
// Void the incorrect journal
await journal.void('Entry error - wrong amount');

// Create new correcting journal
const corrected = await journals.createWithEntries({...});
await corrected.post();

Error: "Account not found"

Cause: Referenced account doesn't exist or hasn't been saved

Solution: Create and save the account before creating journal entries that reference it.

typescript
// Create account first
const account = await accounts.create({
  number: '1000',
  name: 'Cash',
  type: 'asset'
});

// Now use account.id in entries
await journals.createWithEntries({
  entries: [{ accountId: account.id, ... }]
});

Error: "Entry cannot have both debit and credit"

Cause: Entry validation failed

Solution: Each entry must be debit-only OR credit-only (never both). Set the unused side to 0.

typescript
// Correct: debit-only
{ accountId: cashId, debit: 100, credit: 0 }

// Correct: credit-only
{ accountId: salesId, debit: 0, credit: 100 }

// WRONG: both sides
{ accountId: cashId, debit: 100, credit: 50 } // ✗ Error

Issue: Unexpected balance calculation

Cause: Including draft or voided journals in calculation

Solution: Only posted journals affect balances. Draft and voided journals are excluded. Check journal status with isPosted().

typescript
// Only posted journals count
const balance = await entries.getAccountBalance(accountId);

// Check which journals are included
const posted = await journals.findPosted();
console.log('Posted journals:', posted.length);

Issue: Balance includes future transactions

Cause: Missing asOfDate parameter

Solution: Use asOfDate to calculate balance up to specific date.

typescript
// Get balance as of January 31st
const janBalance = await entries.getAccountBalance(
  accountId,
  new Date('2025-01-31')
);

// Get current balance (all posted entries)
const currentBalance = await entries.getAccountBalance(accountId);

Issue: Cannot create child account

Cause: Parent account hasn't been saved yet

Solution: Call save() on parent before calling createChild().

typescript
// Create and save parent first
const parent = await accounts.create({
  number: '1000',
  name: 'Assets',
  type: 'asset'
});

// Now create child
const child = await parent.createChild({
  number: '1100',
  name: 'Current Assets'
});

Related Modules