Chart of Accounts (COA)

v0.1.0

Hierarchical COA trees, asset/liability/equity/revenue/expense categorization, leaf-only posting validation, and aggregator rolls.

Chart of Accounts (COA)

The Chart of Accounts (COA) is the structural foundation of the General Ledger. In Obelawium Accounting, accounts are organized as an arbitrary-depth tree where only leaf accounts are permitted to receive transaction postings.

All account management is performed via the ium()->accounting()->accounts() service accessor.


Account Types & Normal Balances

Obelawium recognizes the five canonical statutory account classes:

Account Type EnumNormal BalanceIncreases OnDecreases OnFinancial Statement
AccountType::AssetDebitDebitCreditBalance Sheet
AccountType::LiabilityCreditCreditDebitBalance Sheet
AccountType::EquityCreditCreditDebitBalance Sheet
AccountType::RevenueCreditCreditDebitIncome Statement (P&L)
AccountType::ExpenseDebitDebitCreditIncome Statement (P&L)

Aggregator Nodes vs. Leaf Posting Accounts

An essential domain invariant in ium-accounting is leaf-only posting:

1000 - Assets (Aggregator - Cannot accept postings)
└── 1100 - Current Assets (Aggregator - Cannot accept postings)
    ├── 1110 - Cash & Bank (Aggregator - Cannot accept postings)
    │   ├── 1111 - Operating Checking Account (Leaf - Accepts Postings)
    │   └── 1112 - Payroll Checking Account (Leaf - Accepts Postings)
    └── 1120 - Accounts Receivable (Leaf - Accepts Postings)
  • Aggregators (is_leaf = false): Used for organization, reporting, and rolled-up trial balance sub-totals. Attempting to post to an aggregator throws NonPostingAccountException.
  • Leaf Accounts (is_leaf = true): The deepest node in any branch. Only leaf accounts can be referenced in journal voucher lines.
  • Automatic Classification: When you add a child account to an existing leaf account, the system automatically flips the parent to is_leaf = false, ensuring tree validity at all times.

Creating Accounts

Accounts are created using CreateAccountDto:

use Obelaw\Ium\Accounting\Data\CreateAccountDto;
use Obelaw\Ium\Accounting\Enums\AccountType;

// 1. Create a root aggregator for Current Assets
$currentAssets = ium()->accounting()->accounts()->create(
    new CreateAccountDto(
        code: '1100',
        name: 'Current Assets',
        type: AccountType::Asset,
        currencyCode: 'USD',
        parentId: null, // Root node
        description: 'Liquid assets and receivables'
    )
);

// 2. Create a child leaf posting account
$bankAccount = ium()->accounting()->accounts()->create(
    new CreateAccountDto(
        code: '1111',
        name: 'JPMorgan Chase - Operating 0492',
        type: AccountType::Asset,
        currencyCode: 'USD',
        parentId: $currentAssets->id,
        description: 'Primary corporate disbursement account'
    )
);

Retrieving and Traversing the COA Tree

Fetching the Full Nested Tree

// Returns a hierarchical Collection where parent accounts include nested ->children
$tree = ium()->accounting()->accounts()->tree();

foreach ($tree as $rootAccount) {
    echo "{$rootAccount->code} - {$rootAccount->name}\n";
    foreach ($rootAccount->children as $child) {
        echo "  ├── {$child->code} - {$child->name} (Leaf: " . ($child->is_leaf ? 'Yes' : 'No') . ")\n";
    }
}

Finding an Account by Code or ID

// By code
$account = ium()->accounting()->accounts()->findByCode('1111');

// By ID
$account = ium()->accounting()->accounts()->find($accountId);

// Check if direct posting is allowed
if (!$account->isPosting()) {
    throw new RuntimeException("Cannot post to non-leaf aggregator: {$account->code}");
}

Account Balance Verification

To inspect an account’s running balance:

$balanceDto = ium()->accounting()->accounts()->getBalance($accountId, $periodId = null);

echo "Account: {$balanceDto->accountCode}\n";
echo "Total Debit (minor): {$balanceDto->debitMinor}\n";
echo "Total Credit (minor): {$balanceDto->creditMinor}\n";
echo "Net Balance (minor): {$balanceDto->netBalanceMinor}\n";
echo "Formatted: {$balanceDto->formatted}\n";