Skip to main content
A JournalEntry represents one accounting entry inside a Journal — the atomic unit of double-entry bookkeeping on Well. It belongs to a workspace-scoped Journal (e.g. a SALES, BANK, or PURCHASES journal), carries a human-readable entry number unique within the fiscal year, and is composed of one or more JournalEntryLine child rows that hold the actual debit/credit amounts against ledger accounts. Journal entries flow from two automated posting pipelines: the invoice-journal-entry builder (which creates DRAFT entries when an invoice is extracted) and the payment-journal-entry builder (which creates DRAFT settlement entries from matched bank transactions). Entries can also be written by MCP connectors via the sync mapping pipeline, in which case sourceWorkspaceConnector carries provenance. The lifecycle progresses from DRAFT → VALIDATED → LOCKED; once VALIDATED the entry_number and fiscal context are immutable at the service layer.

API operations

Data model

Attributes

Relationships

System-computed

  • journal_entry_id is generated by PostgreSQL via gen_random_uuid() at INSERT; the TypeScript default randomUUID() is a client-side fallback that is overridden by the DB default.
  • created_at is set by MikroORM onCreate lifecycle hook (new Date()); not client-settable.
  • updated_at is set by MikroORM onCreate + onUpdate lifecycle hooks (new Date()); updated automatically on every flush. DB column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now()) — cannot be null in production.
  • deleted_at follows the platform-wide soft-delete contract: null means active; a non-null timestamp means soft-deleted. All repository queries must predicate deleted_at IS NULL. The posting_idempotency_key partial unique index also gates on deleted_at IS NULL.
  • posting_idempotency_key dedup: the partial unique index journal_entries_workspace_posting_idempotency_unique on (workspace_pk, posting_idempotency_key) WHERE deleted_at IS NULL AND posting_idempotency_key IS NOT NULL prevents double-posting by the automated pipelines. This index lives in Migration20260525102000_journal_entry_posting_metadata, not on the entity decorator — schema:fresh may diverge from production on this column.
  • status defaults to DRAFT at the DB level (DEFAULT ‘DRAFT’). The VALIDATED and LOCKED transitions are enforced at service level (services/accounting/). Once VALIDATED, entry_number is treated as immutable.
  • sourceWorkspaceConnector provenance: when non-null, the entry was written by the MCP sync mapping pipeline (target_model = ‘journal_entry’). A sparse index idx_journal_entries_source_wc exists on source_workspace_connector_pk WHERE non-null — added by Migration20260406100000_expand_mcp_sync_models.
  • Composite unique constraint: (workspace_pk, entry_number, fiscal_year) — enforced at DB level to prevent duplicate entry numbers within the same fiscal year for a workspace.
  • Partial index on (workspace_pk, entry_date) — idx_journal_entries_workspace_date — optimises date-range queries within a workspace.
  • source_entity_type / source_entity_id form a polymorphic soft FK pattern (no DB foreign key). They are set by the posting pipelines to trace origin (e.g. ‘invoice’ + the connector’s external invoice id). No ON DELETE cascade.
  • JournalEntryLine children are the load-bearing accounting rows; the JournalEntry header is the grouping container. Accessing lines via Hasura uses the alias ‘lines’ (not ‘journal_entry_lines’) per the composites.yml documentation.
  • journal_entry_posting_attempts is a separate append-only ledger table (Migration20260526020000) that records each posting attempt (source_kind × source_id × status). It is not a property of JournalEntry itself but references it by source.

Example

Source: apps/api/src/database/entities/JournalEntry.ts · domain: financial-graph · tier: Main