# Chart of accounts

> The one object all three broadly agree on, and still the one that most often breaks the profit and loss. They differ on hierarchy, on whether the account number exists at all, and on how finely the type is stated.

Source: https://lopifi.com/apis/chart-of-accounts
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

**How does each ledger model an account?** Key on the source id, display the account number, and keep the source's own type verbatim alongside your normalised one.

## Divergences between Xero, QuickBooks Online and NetSuite

### Xero has no account hierarchy

Xero's chart is a flat list: no parent field, no grouping object. The grouping in Xero's own reports comes from account type. QBO nests five levels deep in FullyQualifiedName; NetSuite nests arbitrarily.

**Handling:** Render a flat list gracefully, with type as the only grouping. No chart-of-accounts view may assume parents exist.

### Account numbers are not identifiers

They are editable in all three, optional on Xero bank accounts (Xero enforces Code as unique and requires it elsewhere), and in QBO absent entirely unless the company turned account numbers on. Unique is not the same as stable. A mapping table keyed on account code will silently break when a bookkeeper renumbers the chart.

**Handling:** Key every mapping on the source id. Display the number, sort by it, let users search it — but never join on it.

### Type granularity does not survive a lowest-common-denominator mapping

All three separate receivables from other current assets at the type level — QBO AccountType 'Accounts Receivable' vs 'Other Current Asset', NetSuite AcctRec vs OthCurrAsset. QBO's AccountSubType (~80 values) gives the finest granularity of the three, below that. Xero's Type is coarser but carries SystemAccount, which QBO lacks. Collapsing all three onto one enum loses different information in each direction.

**Handling:** Map to the coarsest common type for statement grouping, and keep the source type verbatim in a second field for tooltips, mapping UIs and support questions.

### Normal balance is not a field anywhere

None of the three tells you whether an account is debit-normal. All three expect you to derive it from the class — asset and expense are debit-normal; liability, equity and revenue are credit-normal — and then invert it for contra accounts: accumulated depreciation, allowance for doubtful accounts, treasury stock, sales returns and discounts. Class alone gets every one of those wrong.

**Handling:** Derive once, at normalisation, from class plus a contra flag taken from the source subtype (QBO AccumulatedDepreciation, DiscountsRefundsGiven…) or your own mapping — never class alone — and store it. Deriving it at render time in a component is how a revenue figure ends up with the wrong sign.

## Canonical shape

```ts
type Account = {
  id: string;
  number?: string;              // display only — never a join key
  name: string;
  path?: string[];              // ["Operating expenses", "Travel"]; single element when flat
  parentId?: string;            // undefined on Xero, always
  type: AccountType;            // our taxonomy, coarsest common
  sourceType: string;           // theirs, verbatim: "DIRECTCOSTS" | "OtherCurrentAsset" | "COGS"
  class: "asset" | "liability" | "equity" | "revenue" | "expense";
  normalBalance: "debit" | "credit";   // class, inverted when isContra — at normalisation
  isContra: boolean;            // from source subtype or own mapping: accum. depreciation, allowance…
  isSystem: boolean;            // never offer as a posting target
  currency?: string;
  active: boolean;
  entityIds: string[];          // NetSuite subsidiary scoping; [tenant] elsewhere
  source: SourceRef;
};
```
