Protected ReadonlydbProtected ReadonlysearchProtected ReadonlytimeseriesStaticinstanceThe process service: the shared database and the search cache.
ProtectedbustSelf-bust the read caches after a write on the writer subclass. Three independent busts:
get entry for this row, by exact key (all-mode correct —
works in memory mode, where tag invalidation no-ops). The key must match
what search computes for get({ id }): includeHidden true, all other
filters undefined (dropped by serviceCacheKey), page 1, pageSize 1.A reconciliation-only update (a BANK_INTERNAL_TRANSFER stamp) does not move
the gross implied balance, so bust 3 is sometimes redundant. Busting on
every write anyway is correct and cheap; discriminating would be a way to be
wrong.
Settled, not sequenced, and never rethrown. These used to be three bare
awaits, which coupled them twice over: a rejection in the first skipped
the other two, and then escaped into the caller. create publishes
backoffice.bankTransaction.created BEFORE it busts (see the if (created)
block), so a cache failure rejected a write that had already committed and
already alerted — the operator got a Slack notification for a row the
dashboards would not show until the L2 entry aged out
(BANK_TX_SEARCH_L2_TTL_MS, 24h). Neither the commit nor the publish
can be undone here, which makes throwing strictly worse than serving stale.
OptionalaccountNumber?: stringOptionalaccountTypes?: BankAccountType[]Rows whose account the bank_account register types as one of these.
Optionalamount?: stringOptionalbankIds?: string[]OptionalbankTransactionIds?: string[]OptionalcreatedAt?: DbTimestampCriteriaOptionalid?: string[]OptionalincludeHidden?: booleanHidden rows are left out unless this is true.
OptionalmethodDataKeys?: (Rows whose methodData holds a non-empty value under every one of these keys.
Optionalmethods?: (Filter on methodData.method — e.g. only the ACH debit postings.
OptionalmissingMethodDataKeys?: (Rows whose methodData holds no value under any of these keys; "" counts as none.
OptionalorderBy?: DbOrderByCriterion<Optionalpage?: numberDefaults to 1
OptionalpageSize?: numberDefaults to 10
OptionalpostedAt?: DbTimestampCriteriaWhen the BANK posted the transaction — the only one of the three
timestamps that is the bank's fact rather than ours. Correlating an
external record (a wire notification, a statement line) against this
table has to window on it: createdAt is when CFX happened to write the
row, which for a hand-keyed row can be days off the money movement.
Optionalq?: stringFree-text filter: case-insensitive substring match on
bankTransactionId, description, and statementDescription; when it
parses as a positive decimal it also matches the exact amount.
OptionalreconciliationKeys?: (Rows whose reconciliation carries any of these keys.
OptionalroutingNumber?: stringOptionalsources?: BankTransactionRawSource[]Filter on raw.source — which producer wrote the row.
A feed that resumes from its own high-water mark MUST scope by this. The
mark answers "how far has THIS feed got", and every producer writes to the
same table: an unscoped mark can be set by a row the feed will never
deliver, and since the feeds page forward from it, whatever it skipped is
skipped permanently. See getRouteFusionOutboundHighWaterMark.
raw is nullable (rows predating the column, and any producer with no
payload), so a filtered search never returns those — which is correct for
the high-water-mark use, since a row with no recorded producer cannot be
evidence of one's progress.
Optionalstatus?: "POSTED" | "RECONCILED"Optionaltx?: PgAsyncTransaction<NodePgQueryResultHKT, EmptyRelations>Optionaltypes?: ("CREDIT" | "DEBIT")[]OptionalupdatedAt?: DbTimestampCriteriaThe account's IMPLIED balance (credits − debits over its whole
bank_transaction history) sampled at the CLOSE of every day in the
window — the series behind the balance-over-time chart on the admin
bank-account detail panel.
Correlated on the same key as the bank_account_view balance lateral
(bankAccountBalanceLateralSql), and leaves out a virtual account's sweeps
to its pool as the view does, so the last point equals the panel's
"Implied balance" figure exactly. A drift between the two would mislead an
operator reconciling the account, which is why the correlation predicate is
shared verbatim rather than re-derived.
Windows and buckets on posted_at — the bank's fact. created_at is when
CFX happened to write the row, which for a hand-keyed transaction can be
days off the money movement.
items is empty (rather than a flat zero line) when the key has NO matching
transactions at all: § 6 of the bank_account_view design distinguishes
"cannot be measured / nothing observed" from "measured as zero", and the
chart must carry that distinction too.
Optionalmemo?: stringFFC memo of a shared-FBO (deposit-memo) account. Narrows the correlation key to that memo's flow through the shared account.
OptionaltimeZone?: stringIANA timezone (e.g. "America/Chicago"). Aligns day boundaries and
localDate output to this timezone. Defaults to "UTC".
OptionalincludeHidden?: booleanHidden rows are left out unless this is true.
OptionalincludeManual?: booleanOptionaltx?: PgAsyncTransaction<NodePgQueryResultHKT, EmptyRelations>Removes a dead rail record's marker — the inverse of update, whose ||
jsonb merge is additive and has no delete channel. Idempotent.
Set arithmetic on the marker lives in stripRailIdFromReconciliation, which
is pure and DB-free; SQL's share is the predicate and one jsonb_set.
Releasing a marker is the one write that needs no event publisher, so it sits beside
BankTransactionServicerather than on it: a caller can release a bank transaction with the{ db, valkeyClient }it already holds, where the full service would push an SNS topic ARN — and its secrets — into composition roots that have no internal event publisher today.