Skip to main content
A Transaction represents a single monetary movement recorded against a bank account — a debit or credit of funds initiated by a connector sync (e.g. Plaid, Qonto/PSD2) or created manually. Every transaction belongs to a Workspace and is anchored to an AccountBalance; the two legs of the movement are modelled as a debtor PaymentMeans (source of funds) and a creditor PaymentMeans (destination). Transactions are the primary evidence surface for counterparty-bank discovery, AI categorisation, invoice reconciliation, and the accounting journal-entry pipeline.

API operations

Data model

Attributes

Relationships

System-computed

  • transaction_id: generated via gen_random_uuid() PostgreSQL function at INSERT time; unique constraint enforced at DB level.
  • created_at: set by MikroORM onCreate lifecycle hook (new Date()); never writable via API.
  • updated_at: set by both onCreate and onUpdate lifecycle hooks; reflects latest flush timestamp.
  • deleted_at: soft-delete sentinel; NULL on active records. Set by service layer on deletion; never physically removed. All active queries must carry a deleted_at IS NULL predicate.
  • category_source invariant: when category_source is set to ‘user’ the service layer forces category_confidence to NULL, regardless of payload content, to prevent provenance forgery via Hasura or direct API calls.
  • category_confidence invariant: DB CHECK (transactions_category_confidence_range) enforces category_confidence ∈ [0, 1]; DB CHECK (transactions_category_source_confidence_invariant) enforces that category_confidence IS NOT NULL if and only if category_source = ‘classifier’.
  • category_normalized invariant: DB CHECK (transactions_category_normalized_length) enforces length(category_normalized) ∈ [1, 200] when non-null; DB CHECK (transactions_category_normalized_provenance) enforces category_normalized IS NULL OR category_source IS NOT NULL — a non-null label always requires a non-null source.
  • transaction_external_id dedup: connector sync resolves existing rows via transaction_external_id IN (…) batch lookup (idx_transactions_external_id index) before creating new ones.
  • sourceWorkspaceConnector provenance: non-null value records which WorkspaceConnector created this row; NULL indicates manual/non-connector origin. The raw_data parsing strategy must branch on the originating connector before reading connector-specific fields.
  • instructed_amount is the canonical amount column: used for ABS() aggregation in counterparty-bank scoring queries, micro-deposit fingerprint detection, and large-transaction detection (Q8/Q12). Settlement amount is the FX-converted equivalent and is always in a different or equal currency.
  • Partial indexes: idx_transactions_account_balance_active (WHERE deleted_at IS NULL AND account_balance_pk IS NOT NULL); idx_transactions_workspace_executed_active (WHERE deleted_at IS NULL); idx_transactions_classifier_confidence (WHERE category_source = ‘classifier’ AND deleted_at IS NULL, on workspace_pk + category_confidence — serves the classifier review queue). All exclude deleted rows from hot-path index scans.
  • type and status enum storage: these columns are TEXT with CHECK IN (…) constraints (not native PG enums). MikroORM @Enum without nativeEnumName stores the enum VALUE string (long description), not the enum key. When filtering in SQL, use the description string, e.g. WHERE type = ‘General payments to vendors or suppliers’, not WHERE type = ‘PAYMENT’.

Example

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