Documentation Index
Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
Use this file to discover all available pages before exploring further.
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.
| Naming | Value |
|---|
| Object | Journal Entry |
Resource type (JSON:API type) | journal_entry |
| Collection / records root | journal_entries |
| REST base | /v1/journal-entries |
| Entity class | JournalEntry |
API operations
| Operation | Method & path | Status |
|---|
| List | GET /v1/journal-entries | ✅ Implemented |
| Retrieve | GET /v1/journal-entries/{id} | ✅ Implemented |
| Create | POST /v1/journal-entries | 🟡 Planned |
| Update | PATCH /v1/journal-entries/{id} | 🟡 Planned |
| Delete | DELETE /v1/journal-entries/{id} | 🟡 Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|
| journal_entry_id | string, UUID | ✅ Yes | unique; defaultRaw: gen_random_uuid() | — | Public identifier for the journal entry. Generated by PostgreSQL on insert. Used in all external API references. |
| entry_number | string | ✅ Yes | length ≤ 50; composite unique (workspace_pk, entry_number, fiscal_year). Immutable once status = VALIDATED (enforced at service layer, not DB). | — | Human-readable accounting reference number assigned by the posting pipeline or connector (e.g. VTE-2026-0042). Must be unique within the workspace × fiscal_year pair. |
| entry_date | string (ISO date, YYYY-MM-DD) | ✅ Yes | columnType: date | — | Accounting date of the entry. Determines which fiscal period and year it falls into. Indexed together with workspace for date-range queries. |
| label | string | ⚪ No | length ≤ 500; nullable | — | Free-text description of the entry (e.g. invoice reference, payment description). Shown in the accounting ledger UI. |
| status | string (enum: JournalEntryStatusEnum) | ✅ Yes | default: DRAFT; nativeEnumName: journal_entry_status_enum | DRAFT, VALIDATED, LOCKED | Lifecycle state of the entry. DRAFT = editable; VALIDATED = frozen (entry_number immutable, validated_at/validated_by set); LOCKED = archived, cannot be modified or reversed. |
| validated_at | string (ISO 8601 timestamp) | ⚪ No | nullable | — | Timestamp when the entry was moved to VALIDATED status. Null while status is DRAFT or LOCKED. |
| fiscal_year | integer | ✅ Yes | columnType: integer; part of the composite unique constraint (workspace_pk, entry_number, fiscal_year) | — | Four-digit fiscal year the entry belongs to (e.g. 2026). Combined with entry_number to guarantee uniqueness per workspace. |
| fiscal_period | integer | ⚪ No | nullable; CHECK: fiscal_period IS NULL OR (fiscal_period >= 1 AND fiscal_period <= 13) | 1–13 or null | Accounting period within the fiscal year. Supports up to 13 periods (some jurisdictions use a 13th adjustment period). Null when period-level granularity is not required. |
| source_entity_type | string | ⚪ No | length ≤ 100; nullable. Soft FK — no database foreign key constraint. | — | Polymorphic soft reference: the entity type that triggered this entry (e.g. ‘invoice’, ‘transaction’). Used together with source_entity_id to trace the posting origin without a hard FK. |
| source_entity_id | string | ⚪ No | length ≤ 50; nullable. Soft FK — no database foreign key constraint. | — | Polymorphic soft reference: the public identifier of the entity that triggered this entry (e.g. the external invoice id from the connector). Paired with source_entity_type. |
| posting_idempotency_key | string | ⚪ No | length ≤ 160; nullable. Partial unique index on (workspace_pk, posting_idempotency_key) WHERE deleted_at IS NULL AND posting_idempotency_key IS NOT NULL — lives in migration, not the entity decorator. | — | Deduplication key used by the automated posting pipelines to prevent re-posting the same source event. A second attempt with the same key will conflict at DB level rather than create a duplicate entry. |
| posting_metadata | object (JSONB) | ⚪ No | nullable; type: jsonb | — | Arbitrary structured metadata attached by the posting pipeline at creation time (e.g. builder version, trigger context). No schema enforcement at the DB level. |
| created_at | string (ISO 8601 timestamp), 🔒 system | ✅ Yes | onCreate: () => new Date() | — | Timestamp set automatically by MikroORM lifecycle hook when the record is first persisted. Not client-settable. |
| updated_at | string (ISO 8601 timestamp), 🔒 system | ✅ Yes | onCreate + onUpdate: () => new Date(). DB column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now()). | — | Timestamp updated automatically on every persist. Tracks last modification time. Not client-settable; NOT NULL at DB level. |
| deleted_at | string (ISO 8601 timestamp) | ⚪ No | nullable; soft-delete sentinel | — | When non-null the entry is soft-deleted. All repository queries must filter deleted_at IS NULL. The posting_idempotency_key partial unique index also gates on this column. |
Relationships
| Name | Type | Required | Description |
|---|
| workspace | to-one (workspace) | ✅ Yes | Workspace that owns this journal entry. Mandatory tenant-scope relation. All queries must filter through this relation. FK: workspace_pk → core_api.workspaces(pk). |
| journal | to-one (journal) | ✅ Yes | The Journal (e.g. SALES, BANK, PURCHASES) this entry belongs to. Determines the accounting category and default ledger accounts. FK: journal_pk → core_api.journals(pk). |
| validated_by | to-one (people) | ⚪ No | The People record of the user who validated the entry (set when status transitions to VALIDATED). Nullable. FK: validated_by_pk → core_api.peoples(pk). |
| invoice_transaction | to-one (invoice_transaction) | ⚪ No | The InvoiceTransaction (invoice ↔ bank transaction match) that produced this entry, when created by the payment-settlement posting pipeline. Nullable. FK: invoice_transaction_pk → core_api.invoice_transactions(pk). |
| lines | to-many (journal_entry_line) | — | The JournalEntryLine children that constitute the double-entry: each line carries a debit or credit amount against a ledger account. Accessed via Hasura as the ‘lines’ array_relationship. A balanced entry requires at least two lines with equal total debits and credits. |
| sourceWorkspaceConnector | to-one (workspace_connector) | ⚪ No | The WorkspaceConnector instance (MCP sync connector) that created this entry, when it was ingested via the MCP sync mapping pipeline. Null for entries created by the internal posting pipelines or manually. FK: source_workspace_connector_pk → core_api.workspace_connectors(pk) ON DELETE SET NULL. Sparse index on this column where non-null. |
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
{
"data": {
"type": "journal_entry",
"id": "a3f7c21e-84b2-4d19-9f01-2c8e6b5d3a7f",
"attributes": {
"journal_entry_id": "a3f7c21e-84b2-4d19-9f01-2c8e6b5d3a7f",
"entry_number": "VTE-2026-0042",
"entry_date": "2026-05-15",
"label": "Facture Acme SAS – vente de licences Q2",
"status": "VALIDATED",
"validated_at": "2026-05-16T09:14:22.000Z",
"fiscal_year": 2026,
"fiscal_period": 5,
"source_entity_type": "invoice",
"source_entity_id": "inv_8f3a12cd",
"posting_idempotency_key": "invoice:3b6f1a7c-98d2-4e11-a0c3-7f2e8b4d5c1a:v1",
"posting_metadata": {
"builder_version": "2.1.0",
"triggered_by": "invoice_extraction"
},
"created_at": "2026-05-15T14:32:11.000Z",
"updated_at": "2026-05-16T09:14:22.000Z",
"deleted_at": null
},
"relationships": {
"workspace": {
"data": { "type": "workspace", "id": "d4e9c2a1-5b7f-4c3e-8d2a-1f6b9e3c7a5d" }
},
"journal": {
"data": { "type": "journal", "id": "c1b8e7f2-3a4d-4f9e-8c1b-5d7a2e9f4b3c" }
},
"validated_by": {
"data": { "type": "people", "id": "e5f2a3b1-7c4d-4e8f-9a2b-3c6d1e5f7a8b" }
},
"invoice_transaction": {
"data": { "type": "invoice_transaction", "id": "b7d3e1f4-2a5c-4b8e-9f3d-1a6c2e7b4d5f" }
},
"sourceWorkspaceConnector": {
"data": null
},
"lines": {
"data": [
{ "type": "journal_entry_line", "id": "f2a1b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c" },
{ "type": "journal_entry_line", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
]
}
}
}
}
Source: apps/api/src/database/entities/JournalEntry.ts · domain: financial-graph · tier: Main