beaver-accounting
v0.1.6
Published
Core library for Beaver Accounting - A double-entry accounting system
Maintainers
Readme
beaver-accounting
A production-ready TypeScript library implementing double-entry accounting backed by PostgreSQL.
Features
- Double-entry bookkeeping with debit/credit validation
- Hierarchical chart of accounts (Asset, Liability, Equity, Revenue, Expense)
- Fiscal period management with open/closed status
- Journal entry posting with automatic entry numbering
- Reversal entries linked back to originals
- General ledger, account balances, and trial balance
- Balance sheet and income statement generation
- Retained earnings calculation
- Full audit trail with actor and action tracking
- PostgreSQL-backed with transactional writes and connection pooling
- CLI for running database migrations
Prerequisites
- Node.js >= 20
- PostgreSQL (any recent version)
Installation
npm install beaver-accountingSetup
Configure the database connection via environment variables:
| Variable | Default | Description |
|-----------------------------|----------------------|------------------------------------------|
| PGHOST | localhost | PostgreSQL host |
| PGPORT | 5432 | PostgreSQL port |
| PGDATABASE | beaver_accounting | Database name |
| PGUSER | postgres | Database user |
| PGPASSWORD | (empty) | Database password |
| PGSSL | false | Enable SSL (true / false) |
| PGSSL_REJECT_UNAUTHORIZED | true | Reject invalid SSL certs (set false in dev) |
| PGMAX_CONNECTIONS | 10 | Connection pool size |
| PG_CONNECTION_TIMEOUT | 30000 | Connection timeout in ms |
| PG_QUERY_TIMEOUT | 0 | Query timeout in ms (0 = disabled) |
| PG_RETRY_ATTEMPTS | 3 | Connection retry attempts |
| PG_RETRY_DELAY | 1000 | Delay between retries in ms |
Database Migrations
Run migrations before using the library. The CLI reads connection settings from the environment variables above.
# Apply all pending migrations
npx beaver-accounting migrate
# Check migration status
npx beaver-accounting migrate:status
# Create a new migration file
npx beaver-accounting migrate:create <name>
# Rollback the most recent migration
npx beaver-accounting migrate:rollback
# Rollback multiple migrations
npx beaver-accounting migrate:rollback --steps 3Quick Start
import {
PostgreSQLConnection,
getDatabaseConfig,
AccountingServiceFactory,
AccountType,
LineType,
PeriodStatus,
// Types
type Account,
type FiscalPeriod,
type JournalEntryWithLines,
type BalanceSheet,
type IncomeStatement,
} from 'beaver-accounting';
// 1. Connect to the database
const connection = new PostgreSQLConnection(getDatabaseConfig());
await connection.connect();
// 2. Wire all services from a single factory call
const factory = new AccountingServiceFactory(connection);
const { accountService, journalEntryService } = factory.createAll({
withAuditService: true,
});
// 3. Create accounts
const cash = await accountService.createAccount({
code: '1000',
name: 'Cash',
type: AccountType.Asset,
});
const revenue = await accountService.createAccount({
code: '4000',
name: 'Revenue',
type: AccountType.Revenue,
});
// 4. Post a journal entry (debits must equal credits)
const entry = await journalEntryService.postEntry(
{
fiscalPeriodId: 'your-period-uuid',
entryDate: new Date(),
description: 'Cash sale',
lines: [
{ accountId: cash.id, lineType: LineType.Debit, amount: '500.00' },
{ accountId: revenue.id, lineType: LineType.Credit, amount: '500.00' },
],
},
'user-id', // optional actor ID for audit trail
);
console.log(entry.entryNumber); // e.g. "JE-20260302-0001"
// 5. Reverse a journal entry (if needed)
const reversal = await journalEntryService.reverseEntry(
entry.id,
{ reason: 'Data entry error', reversalDate: new Date() },
'user-id'
);
await connection.disconnect();Money values are always
string(e.g."500.00"). The library uses big.js internally to avoid floating-point errors. Never passnumberfor monetary amounts.
Connection Health & Pool Stats
// Check if the connection is healthy
const isHealthy = await connection.isHealthy(); // true or false
// Get connection pool statistics
const stats = connection.getPoolStats();
// { totalCount: 10, idleCount: 8, waitingCount: 0 }Fiscal Period Management
Fiscal periods are essential for organizing accounting data and preventing changes to historical records.
const { periodRepository } = factory.createAll();
// Create a fiscal period
const period = await periodRepository.save({
name: 'March 2026',
startDate: new Date('2026-03-01'),
endDate: new Date('2026-03-31'),
status: PeriodStatus.Open,
});
// Close a period (prevents posting to it)
await periodRepository.close(period.id, 'user-id');
// Reopen if needed
await periodRepository.reopen(period.id, 'user-id');
// Find periods
const openPeriods = await periodRepository.findOpenPeriods();
const periodByDate = await periodRepository.findByDate(new Date('2026-03-15'));
const allPeriods = await periodRepository.findAll();Core Concepts
| Concept | Description |
|---|---|
| Account | A node in the chart of accounts. Has a type (Asset, Liability, Equity, Revenue, Expense) and a normalBalance auto-derived from the type. Accounts can be nested via parentId. |
| Journal Entry | A balanced set of debit and credit lines. Total debits must equal total credits. Each entry is assigned a sequential entryNumber. |
| Fiscal Period | A named date range (e.g. "March 2026") with a status of OPEN or CLOSED. Posting to a closed period throws PeriodClosedError. |
| Audit Trail | Every mutating action (account created, entry posted, entry reversed) can be recorded with actor ID, actor type, and metadata. Enable with withAuditService: true. |
Services Overview
| Service | Purpose |
|---|---|
| AccountService | Create, update, deactivate, and query accounts in the chart of accounts |
| JournalEntryService | Post new entries, reverse entries, query by period / date range / account |
| LedgerService | Retrieve general ledger lines for an account |
| BalanceService | Calculate account balances for a period or date range |
| TrialBalanceService | Generate a trial balance report across all accounts |
| BalanceSheetService | Produce a structured balance sheet (assets, liabilities, equity) |
| IncomeStatementService | Produce a structured income statement (revenue, expenses, net income) |
| RetainedEarningsService | Calculate retained earnings across fiscal periods |
| AuditService | Record and query audit log entries |
All services are available via AccountingServiceFactory.createAll(). Internally, shared instances are reused so that services composing others (e.g. BalanceSheetService depends on BalanceService) do not duplicate work.
API Reference
AccountService Methods
| Method | Description |
|--------|-------------|
| createAccount(options, actorId?) | Create a new account |
| updateAccount(id, options, actorId?) | Update account name, description, parent |
| deactivateAccount(id, actorId?) | Deactivate (requires no journal lines or active children) |
| reactivateAccount(id, actorId?) | Reactivate an inactive account |
| findById(id) / findByCode(code) | Look up accounts |
| findAll(options?) / findActive(options?) | List all or active accounts |
| findByType(type, options?) | Filter by account type |
| getAccountTree() | Get hierarchical tree of all accounts |
| getChildren(parentId) / getRootAccounts() | Navigate hierarchy |
| getDescendants(id) / getAncestors(id) | Get all below/above in tree |
| moveSubtree(id, newParentId, moveChildren?, actorId?) | Move account branch |
JournalEntryService Methods
| Method | Description |
|--------|-------------|
| postEntry(dto, actorId?) | Post a new balanced entry |
| reverseEntry(id, dto, actorId?) | Create reversing entry |
| findById(id) / findByEntryNumber(number) | Look up entries |
| findByPeriod(periodId) | Entries in a fiscal period |
| findByDateRange(start, end) | Entries in date range |
| findByAccount(accountId) | All entries affecting an account |
LedgerService Methods
| Method | Description |
|--------|-------------|
| getAccountLedger(accountId) | Full ledger with running balance |
| getAccountLedgerByPeriod(accountId, periodId, openingBalance?) | Period-specific ledger |
| getAccountLedgerByDateRange(accountId, start, end, openingBalance?) | Date range ledger |
BalanceService Methods
| Method | Description |
|--------|-------------|
| getAccountBalance(accountId) | Current balance (all time) |
| getAccountBalanceAtDate(accountId, date) | Balance as of a date |
| getAccountBalanceByPeriod(accountId, periodId) | Balance for a period |
| getAllAccountBalances(asOfDate?) | Balances for all active accounts |
| getBalancesByType(type, asOfDate?) | Filter by account type |
| getBalanceSummary(asOfDate?) | Complete summary with totals |
Report Services
| Service | Method | Description |
|---------|--------|-------------|
| TrialBalanceService | generateTrialBalance() | Current trial balance |
| | generateTrialBalanceAtDate(date) | As of specific date |
| | generateTrialBalanceByPeriod(periodId) | For a fiscal period |
| BalanceSheetService | generate(asOfDate, options?) | Full balance sheet |
| IncomeStatementService | generate(startDate, endDate, options?) | Income statement |
| RetainedEarningsService | calculate(asOfDate) | Retained earnings total |
| | getBreakdown(asOfDate) | Breakdown by period |
AuditService Methods
| Method | Description |
|--------|-------------|
| record(dto) / recordStrict(dto) | Log an audit entry |
| getEntityHistory(entityType, entityId, limit?) | History for an entity |
| getActorHistory(actorId, limit?) | History by actor |
| getActionsByDateRange(start, end, limit?) | History in date range |
| getCorrelatedEntries(correlationId) | Linked entries (e.g., reversals) |
| query(options) | Flexible query with filters |
Report Generation Examples
const { trialBalanceService, balanceSheetService, incomeStatementService, retainedEarningsService } =
factory.createAll();
// Trial Balance
const trialBalance = await trialBalanceService.generateTrialBalanceAtDate(new Date());
console.log(trialBalance.isBalanced, trialBalance.totalDebits);
// Balance Sheet
const balanceSheet = await balanceSheetService.generate(new Date(), {
includeZeroBalances: false,
});
console.log(balanceSheet.totalAssets, balanceSheet.totalLiabilitiesAndEquity);
// Income Statement
const incomeStatement = await incomeStatementService.generate(
new Date('2026-01-01'),
new Date('2026-03-31'),
);
console.log(incomeStatement.netIncome, incomeStatement.isLoss);
// Retained Earnings
const retainedEarnings = await retainedEarningsService.getBreakdown(new Date());
console.log(retainedEarnings.amount, retainedEarnings.breakdown);Type Definitions
| Type | Description |
|------|-------------|
| Account | { id, code, name, type, normalBalance, parentId, isActive, ... } |
| JournalEntry | { id, entryNumber, fiscalPeriodId, entryDate, description, ... } |
| JournalEntryWithLines | Journal entry with lines: JournalEntryLine[] |
| FiscalPeriod | { id, name, startDate, endDate, status, closedAt, closedBy } |
| AccountBalance | { accountId, balance, balanceType, totalDebits, totalCredits } |
| AccountLedger | { entries: LedgerEntry[], openingBalance, closingBalance, ... } |
| TrialBalance | { lines, totalDebits, totalCredits, isBalanced, ... } |
| BalanceSheet | { currentAssets, fixedAssets, liabilities, equity, totals, ... } |
| IncomeStatement | { revenue, expenses, totalRevenue, totalExpenses, netIncome, ... } |
| AuditLog | { actorId, action, entityType, entityId, before, after, ... } |
Account Code Conventions
The library uses these account code ranges for automatic categorization in financial reports:
| Range | Category | |-------|----------| | 1000-1499 | Current Assets | | 1500+ | Fixed Assets | | 2000-2499 | Current Liabilities | | 2500+ | Long-term Liabilities | | 3000+ | Equity | | 4000+ | Revenue | | 5000+ | Expenses |
CLI Reference
beaver-accounting <command>| Command | Arguments / Options | Description |
|---|---|---|
| migrate | — | Apply all pending migrations |
| migrate:status | — | Show applied and pending migrations |
| migrate:create | <name> | Scaffold a new .sql migration file |
| migrate:rollback | --steps <n> (default 1) | Roll back the last n migrations |
Error Handling
All errors are named exports that extend AccountingError. Import them directly for typed catch blocks.
import {
PeriodClosedError,
BalanceError,
AccountNotFoundError,
JournalEntryNotFoundError,
ValidationError,
DuplicateAccountCodeError,
} from 'beaver-accounting';
try {
await journalEntryService.postEntry(dto);
} catch (err) {
if (err instanceof PeriodClosedError) {
// Attempted to post into a closed fiscal period
} else if (err instanceof BalanceError) {
// Debits do not equal credits
} else if (err instanceof ValidationError) {
// Field-level validation failure — err.errors contains details
} else {
throw err;
}
}All exported error classes:
AccountingError · BalanceError · AccountNotFoundError · PeriodClosedError · PeriodNotFoundError · DuplicateEntryError · ValidationError · DuplicateAccountCodeError · AccountHasJournalLinesError · AccountHasChildrenError · CircularReferenceError · ParentNotFoundError · ParentInactiveError · JournalEntryNotFoundError · EntryAlreadyReversedError
License
MIT
