Ledger Account

A LedgerAccount represents a single line in a workspace's chart of accounts (CoA), classified by accounting type (ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE) and grouped into account classes 1–9. It is the foundational node of the accounting graph: invoice items, journal entry lines, and workspace posting mappings all FK into it. Ledger accounts may be arranged in a parent–child hierarchy (one self-referential ManyToOne), and they can be marked as auxiliary (linked to a customer, supplier, or employee counterparty dimension). The connector sync pipeline writes ledger accounts from external accounting tools (Xero, Pennylane, etc.) and stamps each row with source_workspace_connector_pk for provenance tracking.

NamingValue
ObjectLedger Account
Resource type (JSON:API type)ledger_account
Collection / records rootledger_accounts
REST base/v1/ledger-accounts
Entity classLedgerAccount

API operations

OperationMethod & pathStatus
ListGET /v1/ledger-accountsβœ… Implemented
RetrieveGET /v1/ledger-accounts/{id}βœ… Implemented
CreatePOST /v1/ledger-accounts🟑 Planned
UpdatePATCH /v1/ledger-accounts/{id}🟑 Planned
DeleteDELETE /v1/ledger-accounts/{id}🟑 Planned

Data model

Attributes

FieldTypeRequiredConstraintsAllowed valuesDescription
ledger_account_idstring, UUID, πŸ”’ systemβœ… Yesunique; generated by gen_random_uuid() on INSERTβ€”Public stable identifier for the ledger account. Use this in all API references; the internal pk is never exposed.
account_numberstringβœ… Yesmax length 20; PARTIAL UNIQUE on (workspace_pk, account_number) WHERE deleted_at IS NULL (index idx_ledger_accounts_workspace_account_number_active_unique); the legacy non-partial UNIQUE constraint was replaced by Migration20260529100000β€”The chart-of-accounts code for this account within the workspace, e.g. '411000' (PCG), '1200' (IFRS). Uniqueness is enforced only among active (non-deleted) rows to allow re-creation after a soft-delete.
namestringβœ… Yesmax length 255β€”Human-readable display name for the ledger account.
account_typeenum (ledger_account_type_enum)βœ… Yesnon-nullable; native PostgreSQL enumASSET | LIABILITY | EQUITY | REVENUE | EXPENSEBroad accounting classification per the double-entry model. Drives debit/credit sign convention in journal entries and determines which financial statement section the account appears in.
account_classintegerβœ… YesCHECK (account_class >= 1 AND account_class <= 9); composite index (workspace_pk, account_class)1 – 9Numeric account class (plan comptable class digit). Used to group accounts in chart-of-accounts views and as a fast filter for trial-balance queries. Indexed together with workspace_pk.
is_auxiliarybooleanβœ… Yesdefault falsetrue | falseIndicates whether this account carries an auxiliary (counterparty) dimension. When true, auxiliary_type must be set and journal entry lines on this account must reference a counterparty company.
auxiliary_typeenum (auxiliary_type_enum)βšͺ Nonullable; native PostgreSQL enum; required in practice when is_auxiliary = trueCUSTOMER | SUPPLIER | EMPLOYEEThe counterparty dimension type for auxiliary accounts. CUSTOMER = accounts receivable style, SUPPLIER = accounts payable style, EMPLOYEE = payroll/expense style.
is_activebooleanβœ… Yesdefault truetrue | falseWhether the account is currently open for posting. Inactive accounts remain in the chart for historical reporting but should be excluded from new posting selection lists.
descriptionstringβšͺ Nonullable; TEXT (unbounded)β€”Optional free-text description or instructions for use. Populated by the connector sync when the source accounting tool provides an account note.
created_atDate, πŸ”’ systemβœ… Yesset once on INSERT via MikroORM onCreate hook; TIMESTAMPTZ NOT NULL DEFAULT now()β€”Timestamp of row creation. Never modified after insert.
updated_atDate, πŸ”’ systemβœ… YesTIMESTAMPTZ NOT NULL DEFAULT now() per migration; set on INSERT and on every UPDATE via MikroORM onUpdate hookβ€”Timestamp of last modification. The database column is NOT NULL (baseline migration defines it as TIMESTAMPTZ NOT NULL DEFAULT now()). Updated automatically on every PATCH/flush.
deleted_atDate | null, πŸ”’ systemβšͺ Nonullable; soft-delete sentinel; all queries must filter WHERE deleted_at IS NULLβ€”Soft-delete timestamp. When non-null the row is logically deleted. Hard deletes are never performed. The partial unique index on (workspace_pk, account_number) ignores rows where deleted_at IS NOT NULL, so a previously deleted account number can be re-created.

Relationships

NameTypeRequiredDescription
workspaceto-one (workspace)βœ… YesThe workspace that owns this ledger account. Enforces tenant isolation. All queries must scope to workspace_pk. Indexed via the composite (workspace_pk, account_class) and (workspace_pk, parent_account_pk) indexes.
parent_accountto-one (ledger_account)βšͺ NoSelf-referential parent in the chart-of-accounts hierarchy. NULL for top-level accounts (e.g. class root '400000'). Indexed on (workspace_pk, parent_account_pk) β€” index idx_ledger_accounts_workspace_parent created by Migration20260306100000 β€” to support efficient subtree traversal.
child_accountsto-many (ledger_account)β€”Inverse of parent_account. Collection of all direct child accounts in the hierarchy. Lazy-loaded via MikroORM Collection.
source_workspace_connectorto-one (workspace_connector)βšͺ NoThe WorkspaceConnector instance (e.g. a Xero or Pennylane sync) that last wrote or created this ledger account row. NULL for manually created accounts. Added by Migration20260406100000_expand_mcp_sync_models (that migration covers ledger_accounts; Migration20260331100000 only added the column to companies and peoples). ON DELETE SET NULL so connector removal does not cascade-delete ledger accounts. Partial index idx_ledger_accounts_source_wc (WHERE source_workspace_connector_pk IS NOT NULL) added by the same migration.

System-computed

  • ledger_account_id is generated by PostgreSQL gen_random_uuid() on INSERT and also seeded in-process via randomUUID() from Node crypto as the MikroORM default; it is unique and immutable.
  • created_at is set once via MikroORM onCreate lifecycle hook (new Date()); never written again.
  • updated_at is set on CREATE and on every UPDATE via MikroORM onUpdate lifecycle hook. The database column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now() per Migration20260306100000); the MikroORM entity declares it optional (updated_at?: Date) but that only affects TypeScript nullability β€” the DB enforces NOT NULL.
  • deleted_at is null on creation; set to now() by the application (never by the database) to perform a soft-delete. The partial unique index idx_ledger_accounts_workspace_account_number_active_unique (WHERE deleted_at IS NULL) ensures uniqueness only among active rows β€” soft-deleted rows are excluded, allowing re-insertion of the same (workspace_pk, account_number) pair after a soft-delete.
  • The non-partial UNIQUE(workspace_pk, account_number) constraint from Migration20260306100000 was dropped by Migration20260529100000_ledger_accounts_dedup_unique_idx. That migration also deduped existing active duplicates (keeping the earliest pk) before building the partial index.
  • source_workspace_connector_pk is set by the connector sync pipeline (MCP / Xero / Pennylane / etc.) to record provenance. It is NULL for manually created accounts and is SET NULL (not cascade-deleted) when the workspace_connector row is removed. Column and partial index added by Migration20260406100000_expand_mcp_sync_models.
  • account_class is a human-supplied classification digit (1–9) enforced by a CHECK constraint in the database; it is not derived.
  • is_auxiliary defaults to false; auxiliary_type defaults to NULL. The pipeline or user must explicitly set both when creating an auxiliary account.
  • is_active defaults to true; setting it to false deactivates the account without soft-deleting it, preserving historical journal entry lines.
  • The (workspace_pk, account_class) composite index (idx_ledger_accounts_workspace_class) and the (workspace_pk, parent_account_pk) composite index (idx_ledger_accounts_workspace_parent) are created by Migration20260306100000 and are transparent to the application layer.

Example

{
  "data": {
    "type": "ledger_account",
    "id": "e3a4c9f0-12b7-4d88-9031-cc52a7d1e205",
    "attributes": {
      "ledger_account_id": "e3a4c9f0-12b7-4d88-9031-cc52a7d1e205",
      "account_number": "411000",
      "name": "Clients β€” France",
      "account_type": "ASSET",
      "account_class": 4,
      "is_auxiliary": true,
      "auxiliary_type": "CUSTOMER",
      "is_active": true,
      "description": "Comptes clients β€” marchΓ© domestique France",
      "created_at": "2026-03-10T09:14:22.000Z",
      "updated_at": "2026-05-15T16:03:08.000Z",
      "deleted_at": null
    },
    "relationships": {
      "workspace": {
        "data": { "type": "workspace", "id": "a1b2c3d4-0000-4000-8000-111111111111" }
      },
      "parent_account": {
        "data": { "type": "ledger_account", "id": "b9d00001-aaaa-4bbb-cccc-000000000001" }
      },
      "source_workspace_connector": {
        "data": { "type": "workspace_connector", "id": "f5e60000-dead-beef-cafe-123456789abc" }
      }
    }
  }
}
Source: apps/api/src/database/entities/LedgerAccount.ts Β· domain: financial-graph Β· tier: Main