---
title: "The accounting model"
description: "Every Lopifi component sits on one data model, and if the model is wrong no amount of design saves the product. This page defines it, from the journal line up to basis and materiality."
canonical: "https://lopifi.com/docs/domain/accounting-model"
---

# The accounting model

> Every Lopifi component sits on one data model, and if the model is wrong no amount of design saves the product. This page defines it, from the journal line up to basis and materiality.

Source: https://lopifi.com/docs/domain/accounting-model
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# The accounting model

Every Lopifi component sits on one data model, and if the model is wrong no amount of design saves the product. This page defines it, from the journal line up to basis and materiality.

Most of the UI decisions below follow from one fact:

> **The general ledger and the financial statement are two different representations of the same fact, and they disagree about signs.** A component that doesn't know which representation it's rendering will get revenue backwards.

---

## 1. Double-entry

Every transaction posts at least two lines, and debits equal credits at every level of aggregation.

```ts
type JournalLine = {
  accountId: string;
  /** Exactly one of these is non-zero. Never a single signed amount. */
  debit: Money;
  credit: Money;
  dimensions: Dimensions;
  memo?: string;
};

type JournalEntry = {
  id: string;
  entryNumber: string;        // sequential, gapless, never reused
  date: Date;                 // the transaction date
  periodId: string;           // resolved fiscal period — NOT derived at render time
  lines: JournalLine[];       // ≥ 2
  source: EntrySource;        // see §6
  status: "draft" | "posted" | "reversed";
  reversalOf?: string;        // entry id
  reversedBy?: string;
};
```

### Debit and credit are two columns, never one

It's the most important rendering rule in the accounting family. A signed column (`-1,240.00`) folds two facts into one. Double-entry exists so a person can check the arithmetic by eye, and a single column takes that check away.

**`LedgerTable` and `JournalEntry` render two columns. There is no single-column variant.**

The balance check is displayed, not assumed:
```
                              Debit        Credit
  6200 · Rent expense       4,500.00
  1010 · Operating cash                  4,500.00
                          ─────────    ─────────
                           4,500.00     4,500.00   ✓ balanced
```

An unbalanced entry is a **prominent error state with the difference stated**. It doesn't get saved as a draft without comment, and the difference is never rounded away.

---

## 2. Account types and normal balances

Five types. Each has a *normal balance* — the side that increases it.

| Type | Normal | Increases with | Statement | Typical range |
|---|---|---|---|---|
| **Asset** | Debit | Debit | Balance sheet | 1000–1999 |
| **Liability** | Credit | Credit | Balance sheet | 2000–2999 |
| **Equity** | Credit | Credit | Balance sheet | 3000–3999 |
| **Revenue** | Credit | Credit | P&L | 4000–4999 |
| **Expense** | Debit | Debit | P&L | 5000–8999 |

Sub-classification that the statements depend on:

```
Asset       current · non-current           (cash, AR, prepaid, inventory | PP&E, intangibles, ROU)
Liability   current · non-current           (AP, accrued, deferred rev, current LTD | LTD, lease liability)
Equity      contributed · retained · OCI    (common stock, APIC | retained earnings | CTA, unrealized)
Revenue     operating                       (product, services)
Other       other income · gains            (interest income, FX gain, gain on disposal)
Expense     COGS · opex · other · tax       (direct costs | SG&A, R&D | interest, FX loss | income tax)
```

In the UI, an `AccountPicker` groups by type and sub-classification, shows the account number and **shows the normal balance**, because someone posting to an unfamiliar account needs to know which side increases it. It costs one glyph (`Dr` / `Cr`) and prevents a whole class of mistakes.

### Contra accounts

Accounts whose normal balance is the *opposite* of their type, presented as a deduction from their parent.

| Contra account | Type | Normal | Nets against |
|---|---|---|---|
| Accumulated depreciation | Asset | **Credit** | PP&E |
| Allowance for credit losses (CECL, ASC 326) | Asset | **Credit** | Accounts receivable |
| Sales returns and allowances | Revenue | **Debit** | Gross revenue |
| Treasury stock | Equity | **Debit** | Total equity (cost method) |
| Accumulated amortization | Asset | **Credit** | Intangibles |

**Rendering rule:** a contra account is **never displayed as a negative asset**. It renders in its natural position, indented under its parent, with the account name carrying the meaning, and the net shown on a subtotal line:

```
  1500 · Property, plant & equipment          842,000
  1590 · Accumulated depreciation            (218,400)   ← indented, parenthesized
                                            ─────────
        Net property, plant & equipment       623,600
```

Showing `Accumulated depreciation  218,400` as a positive asset is a classic mistake, and every reviewer spots it.

---

## 3. The sign problem

This is what breaks most finance UIs. In the ledger, revenue is a **credit**. If your data layer represents a line as a single signed number with debits positive, a $600,500 revenue month arrives at your component as `-60050000` — minor units, so cents included. On a P&L, it must display as `600,500`.

That leaves you with **two signs**, and they differ:

```ts
type StatementLine = {
  /** As stored in the GL. Credit-normal accounts are negative in a debit-positive convention. */
  ledgerAmount: Money;
  /** As presented on the statement. Revenue positive, expenses positive, net income positive. */
  presentationAmount: Money;
  /** Which way this line's presentation runs relative to the ledger. */
  presentationSign: 1 | -1;
};
```

**Rules Lopifi enforces:**

1. **Components render `presentationAmount`.** They never negate `ledgerAmount` themselves, and never infer the sign from the account type at render time.
2. **The transformation happens once, in the data layer**, and it is the caller's responsibility. Components that do sign math will eventually disagree with the backend.
3. **`LedgerTable`, `JournalEntry`, `AccountRegister`, and `TrialBalance` render the ledger representation** (debit/credit columns). Everything else renders the presentation representation.
4. **A negative number on a P&L means the line went the other way**, such as a contra-revenue, a credit to an expense account or a negative gross margin. It never comes from the sign convention, and it renders in parentheses like any other negative.

That's why `Money` takes `accounting` (parentheses) as an explicit prop instead of inferring it. The component can't know which representation it's in.

---

## 4. Chart of accounts

```ts
type Account = {
  id: string;
  number: string;               // "6200" — sortable, human-referenced
  name: string;                 // "Rent expense"
  type: AccountType;
  subType: AccountSubType;
  normalBalance: "debit" | "credit";
  parentId?: string;            // hierarchy, max 5 levels
  isContra: boolean;
  contraOf?: string;
  isPostable: boolean;          // header/rollup accounts are NOT postable
  status: "active" | "inactive" | "archived";
  statementMapping: StatementLineId;   // where it lands on the P&L / BS
  requiredDimensions: DimensionKey[];
};
```

**UI rules:**

- **The account number is always shown**, never just the name. Accountants navigate by number, and two accounts can legitimately share a name across entities.
- **Non-postable accounts are visibly disabled** in every picker. Posting to a rollup account is a data-integrity failure that surfaces months later at close.
- **Inactive accounts are hidden by default** but reachable by search, because historical entries reference them.
- **Hierarchy is capped at 5 levels.** Anything deeper is a chart-of-accounts problem. Say so in the empty state instead of supporting infinite nesting.
- **The statement mapping is visible on the account.** "Where does this show up?" is the most common question about an unfamiliar account.

---

## 5. Dimensions

The account is one axis of a modern ledger. Entity, department, customer and the rest are others.

```ts
type Dimensions = {
  entityId: string;             // legal entity — always required
  departmentId?: string;        // cost center
  locationId?: string;
  classId?: string;
  projectId?: string;
  customerId?: string;
  vendorId?: string;
  productId?: string;
  [custom: string]: string | undefined;
};
```

**UI rules:**

- **Entity is always required and always visible.** A number without an entity is meaningless in a multi-entity group, and mixing entities in one total without eliminating intercompany is a misstatement.
- **`requiredDimensions` is enforced at the account level**, not globally. Revenue accounts may require a customer; rent may require a location. The `JournalEntry` form reads it from the account the moment it's picked and adds the fields inline.
- **Every statement, table, and chart declares its dimension filter in its header.** "Operating expenses — Engineering, US entity, Sep 2026" is a different number from "Operating expenses". An unlabeled filtered total is the most common way a dashboard misleads.
- **Segment reporting is a dimension pivot.** `StatementTable` supports a `groupBy: DimensionKey` that turns the period columns into segment columns.

---

## 6. Entry sources and provenance

Where an entry came from determines what a user may do with it.

| Source | Editable? | Typical origin |
|---|---|---|
| `manual` | Yes, until posted | A person typed it |
| `adjusting` | Yes, until posted | Close adjustments |
| `recurring` | Template editable; instances not | A schedule generated it |
| `system` | **No** | Depreciation, amortization, FX revaluation |
| `integration` | **No** | Synced from Xero, QBO, Stripe, a bank |
| `reversal` | **No** | The automatic reverse of another entry |
| `elimination` | Consolidation only | Intercompany eliminations |

**Rules:**

- **Source is displayed on every entry.** A user looking at a discrepancy needs to know whether a human or a sync created it before they know who to ask.
- **Integration-sourced entries are read-only in Lopifi** and link back to their source system with the external ID. Editing a synced entry locally creates a divergence that the next sync will either overwrite or duplicate.
- **A system entry that looks wrong is a configuration problem**, and the UI should route the user to the configuration (the depreciation method, the FX rate source) rather than letting them edit the output.

---

## 7. Immutability

> **A posted entry is never edited and never deleted. It is reversed.**

This is the control that makes a ledger auditable, and the main reason accounting software behaves differently from a spreadsheet.

```
draft    → editable, deletable, not in any statement
posted   → immutable. Appears in statements. Only action: reverse.
reversed → immutable. Appears with its reversal, both visible.
```

**UI rules:**

- **There is no delete action on a posted entry**, disabled or otherwise. A disabled delete button invites a support ticket asking how to enable it.
- **The reverse action explains what it does**, in an `AlertDialog` with hold-to-confirm: "Reverse JE-2026-0841? This creates an offsetting entry dated Sep 30, 2026. The original entry stays in the ledger."
- **A reversed entry and its reversal are visually linked** and both remain visible. Hiding the original is the same as deleting it.
- **A reversal in a closed period is dated in the current open period**, not the original period. The UI must not offer the closed date.
- Drafts are the only place a delete exists, and drafts never appear in a statement.

---

## 8. Cash basis vs accrual basis

The same ledger produces two different sets of statements.

| | Accrual | Cash |
|---|---|---|
| Revenue recognized | When earned | When cash received |
| Expense recognized | When incurred | When cash paid |
| AR / AP | Present | **Absent** |
| Prepaid / accrued / deferred | Present | **Absent** |
| Used for | GAAP/IFRS, audits, most lenders | Tax returns where IRC §448 permits it; small-business management accounts. Not GAAP for any entity — a special purpose framework |

The basis toggle is an **always-visible control on every statement**, never a setting buried in a menu. The two numbers can differ by an order of magnitude, and a reader who can't tell which basis they're looking at is being misled.

What the components do with it:
- On cash basis, AR/AP/prepaid/accrual/deferred lines **disappear entirely**. They don't render as zero, because they don't exist in that view.
- The statement header states the basis, and so does every export and print.
- Switching basis is a **full data change, so nothing animates** (Money Law 2).
- Comparative columns must be on the same basis. Mixing is blocked at the type level.

---

## 9. Materiality

A variance report is useful only if it encodes which differences deserve a look. Two different things share the word, and the UI keeps them apart. **Materiality** is a judgment about whether a misstatement or omission would change the decision of someone relying on the financial statements — it is qualitative as well as quantitative, and no fixed number decides it (SEC SAB 99; FASB Concepts Statement No. 8, ch. 3). A **flux threshold** is a house rule for which period-over-period variances get an explanation in the close. The policy below is a flux threshold. It's set with materiality in mind, and the UI calls it a threshold so nobody mistakes one for the other. (The component that edits it ships as [`MaterialityPolicy`](https://lopifi.com/md/docs/catalog/close-controls-consolidation#144-materialitypolicy); the name is historical, the behavior is the one described here.)

```ts
type FluxThresholdPolicy = {
  /** Absolute floor. Differences below this are ignored entirely. */
  trivial: Money;
  /** A variance is flagged if it exceeds BOTH thresholds. */
  absolute: Money;
  relative: number;         // e.g. 0.05 for 5%
  /** Optional per-account overrides — cash is flagged at any amount. */
  overrides?: Record<string, Partial<FluxThresholdPolicy>>;
};
```

**Rules:**

- **Both thresholds must be exceeded.** A 200% swing on $12 clears the percentage but not the amount — not news. A $40,000 swing on a $40M account clears the amount but is 0.1% — usually noise. A $50,000 swing that is also 12% of the balance clears both, and gets an explanation. Flagging on percentage alone produces a report nobody reads.
- **The thresholds are a business judgment and therefore configurable.** Lopifi ships a default and a UI to change it; it never hard-codes 5%.
- **The threshold in force is displayed** on any surface that flags variances. "Flagging variances over $10,000 and 5%" — so a reviewer knows what they're *not* seeing.
- **A variance under the threshold can still be material.** A small misstatement can matter for qualitative reasons — it turns a loss into a profit, breaches a covenant, involves a related party. That is why cash and related-party accounts commonly override to zero tolerance. Support per-account overrides.

---

## 10. GAAP vs IFRS

Components take a `framework` prop wherever the framework changes presentation, and take no position anywhere else.

| | US GAAP | IFRS |
|---|---|---|
| P&L name | Income Statement | Statement of Profit or Loss |
| BS name | Balance Sheet | Statement of Financial Position |
| BS ordering | Most liquid first (cash → …) | Often least liquid first |
| Inventory | FIFO, LIFO, weighted avg | **LIFO prohibited** |
| Development costs | Generally expensed (ASC 730); some software capitalized (ASC 985-20 for software to be sold; ASC 350-40 for internal-use, where ASU 2025-06 replaces project stages with a probable-to-complete threshold from fiscal 2028) | Capitalized when the IAS 38 criteria are met |
| PP&E | Cost model | Cost **or** revaluation model |
| Extraordinary items | Prohibited | Prohibited |
| OCI presentation | One or two statements | One or two statements |
| P&L structure | Captions by function or nature; ASU 2024-03 adds a footnote disaggregating expense captions (purchases of inventory, employee compensation, D&A) for public entities from fiscal 2027 | IFRS 18 (replacing IAS 1 for periods from Jan 1, 2027) imposes operating, investing and financing categories, required subtotals, and disclosure of management-defined performance measures |

Statement titles, section ordering and the presence of certain lines (revaluation surplus) follow the framework, and the statement header names it. Lopifi **doesn't convert** between frameworks. That's an accounting judgment, and a component only renders.

---

## Cross-references

| Topic | Doc |
|---|---|
| How numbers are formatted, signed, ruled | [`foundations/numerics.md`](https://lopifi.com/md/docs/foundations/numerics) |
| Periods, close states, cutoff | [`periods-and-close.md`](https://lopifi.com/md/docs/domain/periods-and-close) |
| Statement structure and consolidation | [`statement-semantics.md`](https://lopifi.com/md/docs/domain/statement-semantics) |
| Schedules, accruals, roll-forwards | [`schedules-and-accruals.md`](https://lopifi.com/md/docs/domain/schedules-and-accruals) |
| Controls, approvals, audit trail | [`controls-and-audit.md`](https://lopifi.com/md/docs/domain/controls-and-audit) |
| Terminology | [`lexicon.md`](https://lopifi.com/md/docs/domain/lexicon) |
