Account plans
deccf/domainpacks/accountplan adds accounting and budgeting meaning on top of the engine's category references. A plan entry contains an account code, label, classification, normal debit or credit side, and category reference. Plans can also provide a closing flow tailored to their account structure.
A category is the engine's reporting group. An account plan adds account codes and domain metadata; it does not create journal vouchers or a separate double-entry ledger.
When a plan entry is used to create an account, its type and category are copied to the engine's account reference. Postings to income, expense, and balance accounts then inherit the account's category. Income is reported as positive and expense as negative; balance accounts follow their signed debit and credit movements.
Usage
import { cashflow, currencies, defineModel, entity, periods, schedule } from "deccf";
import { stdaccount } from "deccf/domainpacks/accountplan";
const anna = entity.party();
const salaryIncome = stdaccount({ code: "income.employment.salary", owner: anna });
const bank = stdaccount({ code: "assets.cash.bank", owner: anna });
const salary = cashflow({
debit: bank,
credit: salaryIncome,
schedule: schedule.monthly(),
amount: 52_000,
});
const company = entity.asset();
const businessBank = stdaccount({
code: "1930",
owner: company,
initial: 25_000,
});
const businessRevenueAccount = stdaccount({ code: "3000", owner: company });
const businessRevenue = cashflow({
debit: businessBank,
credit: businessRevenueAccount,
schedule: schedule.monthly(),
amount: 10_000,
});
const model = defineModel({
name: "salary and business revenue model",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna, company },
flows: { salary, businessRevenue },
});
return model;stdaccount({ code, owner, initial? }) looks up a code in both account plans and creates an account with the plan's type and category. Use a personal code such as "income.employment.salary" for a person or household, or a business code such as "1930" for a company. Numeric-looking account codes are strings too. It accepts supported account categories and group codes, such as "1000", "1100", or "19", when every account below that category has one account type; mixed-type categories such as "2000" are rejected.
During compilation, a stdaccount reference is matched to an explicitly declared account with the same owner, plan category, and type. That explicit account keeps its name and opening balance. If there is no match, the compiler adds the referenced account to the model automatically. Accounts used in flows, statements, book closing, the household budget, and Swedish tax do not need to be repeated in the model's accounts collection. Use accounts to choose a result name, retain an otherwise-unused opening-balance account, or register a reference used only inside a callback that compilation cannot inspect. An opening value on a stdaccount used in a flow or another discoverable declaration is retained automatically. If the compiler would need to create a parent account while the same owner already has a more specific subaccount below it, it stops with an error identifying both codes.
The Personal account plan includes the detailed expense categories used by the Household budget pack, including personal hygiene, child insurance and equipment, household consumables, home equipment, utilities, subscriptions, media, and home insurance.
category(path) remains available for a model-specific path. In the example, Anna's account categories come from the personal plan. Every row in a posting flow inherits the category of its target account. Since both accounts belong to Anna, the salary flow also infers its owner. The bank is the only asset account in the booking, so it is inferred as the cash account.
normalBalance describes the side recommended by the plan. It does not change the engine's debit and credit rules: account() uses the account's type and category, while the booking side determines whether its signed balance increases or decreases.
Plan entries can also carry tags for reusable selectors. findForAccount(accountRef) identifies an account reference by its plan category and returns the matching plan entry, including those tags. The Business account plan marks profit-and-loss lines, asset and liability groups, financial debt, and debt-service postings. The Business Metrics pack uses those tags to calculate KPIs for each owner without requiring specific model account names.
An account plan can identify an account's owner with ownerForAccount(accountRef). fiscalYearStartMonth(owner) reads the optional numeric entity attribute fiscalYearStartMonth; it defaults to January and accepts values from 1 through 12. fiscalYearFor(owner, date) returns the corresponding fiscal-year start, end, and label. Domain packs use these helpers to publish owner-specific metrics against each company's own financial year.
import { currencies, defineModel, entity, periods } from "deccf";
import { businessAccountPlan, stdaccount } from "deccf/domainpacks/accountplan";
const company = entity.asset({ attributes: { fiscalYearStartMonth: 7 } });
const fiscalYear = businessAccountPlan.fiscalYearFor(company, "2026-09");
const bank = stdaccount({ code: "1930", owner: company, initial: 50_000 });
const model = defineModel({
name: "company-fiscal-year",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-07", count: 12 }),
entities: { company },
accounts: { bank },
});
console.log(fiscalYear);
return model;Category paths are hierarchical. The company plan organizes an account under its class, account group, main account, and subaccount. For example, categoryFor("1000"), categoryFor("1100"), categoryFor("1110"), and categoryFor("1111") return the class, group, main-account, and subaccount categories in that tree. The explicit forms categoryForClass("1") and categoryForGroup("11") are also available. result.category(ref) includes postings on that category and all descendants. categoryTree and categoryLabels expose each node's parent and display name. A run includes labels for the category tree it uses in deterministic.categories, so report code can show parent labels from the result alone.
Account plans do not contain tax rules or tax-return mappings. Swedish tax treatment belongs to the separate tax pack, which can map personal account categories to the declaration boxes it supports.
Equity and personal net worth
The personal-plan entry equity.retained_earnings represents accumulated equity and is labeled Accumulated net worth. It is the personal plan's counterpart to retained earnings in a business plan. It can be used as the balancing account when the model records capital or changes in net worth. Its category path is personal.equity.net_worth.
import { account, currencies, defineModel, entity, periods } from "deccf";
import { personalAccountPlan } from "deccf/domainpacks/accountplan";
const anna = entity.party();
const netWorthDefinition = personalAccountPlan.get("equity.retained_earnings");
const netWorth = account({
owner: anna,
type: netWorthDefinition.type,
category: netWorthDefinition.category,
initial: -250_000, // credit balance under the engine's sign convention
});
const cash = account({
owner: anna,
type: "asset",
category: personalAccountPlan.get("assets.cash.bank").category,
initial: 250_000,
});
const model = defineModel({
name: "anna-net-worth-opening-balance",
currency: currencies.SEK,
timeline: periods.annual({ from: "2026-01", count: 1 }),
entities: { anna },
accounts: { cash, netWorth },
});
return model;The balance is not automatically calculated as assets minus liabilities. Set an opening value or model the bookings that change it. Debit movements increase an account's signed balance; credit movements decrease it.
Book closing
bookClosing creates an ordinary posting flow. Provide an owner; the schedule defaults to an annual close aligned with that owner's fiscalYearStartMonth attribute (January when it is omitted). Set an explicit schedule to override it. During compilation, the plan finds the owner's income and expense accounts by their type, then finds the retained-earnings account using the category defined by the plan. Each scheduled closing reads current balances, clears the income and expense accounts, and transfers the net result to retained earnings.
import { cashflow, currencies, defineModel, entity, periods, schedule } from "deccf";
import { businessAccountPlan, stdaccount } from "deccf/domainpacks/accountplan";
const company = entity.asset();
const bank = stdaccount({ code: "1930", owner: company, initial: 100_000 });
const shareCapital = stdaccount({ code: "2081", owner: company, initial: -100_000 });
const retainedEarnings = stdaccount({ code: "2091", owner: company });
const revenueAccount = stdaccount({ code: "3000", owner: company });
const taxExpense = stdaccount({ code: "8910", owner: company });
const revenue = cashflow({
owner: company,
debit: bank,
credit: revenueAccount,
schedule: schedule.monthly(),
amount: 20_000,
});
const taxPayment = cashflow({
owner: company,
dependsOn: [revenue],
debit: taxExpense,
credit: bank,
schedule: schedule.monthly(),
amount: (ctx) => ctx.get(revenue).amount * 0.2,
});
const closing = businessAccountPlan.bookClosing({
owner: company,
dependsOn: [taxPayment],
dividend: (ctx) => Math.max(0, -ctx.get(retainedEarnings) * 0.2),
contribution: (ctx) => Math.max(0, 100_000 - ctx.get(bank)),
});
const model = defineModel({
name: "business-book-closing",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 72 }),
entities: { company },
accounts: { shareCapital, retainedEarnings },
flows: { revenue, taxPayment, closing },
});
return model;The bank, revenue, and tax expense accounts are collected from their flows; the bank keeps its opening balance from the stdaccount reference even though it is not repeated in accounts. Share capital stays explicit because no flow refers to it. The retained-earnings reference stays explicit because the dividend formula reads its balance with ctx.get(); callbacks are opaque to compilation, so references used only inside a callback must still be registered in accounts.
A dividend debits retained earnings and credits cash. A contribution debits cash and credits the plan's shareholder-contribution account. Amounts can be numbers, input references, or functions. The business plan normally selects account 1930 as cash; set cashAccount only when the model uses another cash account.
Personal-plan closing works similarly for income and expense accounts and equity.retained_earnings, which represents accumulated net worth. The personal plan rejects dividend and contribution because those concepts are not part of that plan.
A closing without a cash payment is a posting flow and does not affect the cashflow series. The schedule controls one-time or repeated closings. Each occurrence reads current balances, so after a year-end closing, the next one closes only the later activity.
The model author chooses ordering through declaration order and dependsOn. The closing module does not know about tax calculations or other special flows.
Plan coverage
The account plans have compile-time reference pages listing every account ID and its English display name: Business account plan accounts and Personal account plan accounts. The business plan's account hierarchy and terminology are inspired by the Swedish BAS chart of accounts; its public labels use English wording and VAT code names.
businessAccountPlan includes all 1,282 unique four-digit account codes in the provided 2026 company account table, with the account class, group, main-account, and subaccount hierarchy. It preserves the table's K2 exclusion marker as notForK2; code 2087 keeps both source names in aliases. The plan's public name remains Business account plan, and account codes and source labels do not change the engine's general accounting concepts.
personalAccountPlan is a separate budgeting and balance structure for households. It distinguishes assets, liabilities, equity, income, and expenses. equity.retained_earnings represents a person's accumulated net worth, analogous to retained earnings in a business plan. The module does not calculate that balance automatically; the model needs to book opening capital and later changes.
The expense.preliminary_income_tax and expense.income_tax_adjustment entries are sibling expense accounts. The Swedish income-tax pack posts monthly advances to the first and books the final tax adjustment to the second. Their categories sit under the shared expenses.tax parent so category summaries can show their total. Personal bookClosing includes both accounts and clears their balances at year-end.
Plans are frozen objects. Define a custom plan with defineAccountPlan({ id, name, version, accounts, closing? }); closing identifies the retained-earnings account and the closing fields supported by the plan. Account codes are plan-specific, and category paths are created beneath the plan's id.
The business account table is inspired by the company account structure published by BAS. The chart's account classes and hierarchy are described in account plan structure and use.