Skip to content

Release: develop -> main#4219

Open
github-actions[bot] wants to merge 7 commits into
mainfrom
develop
Open

Release: develop -> main#4219
github-actions[bot] wants to merge 7 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown

Automatic Release PR

This PR was automatically created after changes were pushed to develop.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

* feat(accounting): add append-only double-entry ledger (monitoring-only)

- new ledger_account/ledger_tx/ledger_leg tables (new-tables-only migration,
  integer-cent balance check, append-only with explicit reversals)
- chart-of-accounts bootstrap and idempotent booking engine with consumers
  for bank, exchange, payin, payout, buy/sell, liquidity and trading sources
- valuation from already-persisted CHF fields and snapshot marks, no new
  external or pricing calls (static isolation gate + DB write isolation test)
- one-time cutover opening, daily mark-to-market and per-account
  reconciliation with staleness guards, transit-age and suspense alarms
- read-only ADMIN endpoints for account balances, ledger detail,
  reconciliation status, suspense and realized-margin reporting
- all crons gated by dedicated process flags (kill switch); existing
  financial log and safety module remain unchanged and authoritative

* fix(accounting): address review findings

- bound cutover opening queries with lookback window and settled filters
- correct check-constraint name to match TypeORM naming strategy
- paginate mark-to-market candidate selection across all open accounts
- align cutover watermark init with per-consumer settled semantics
- harden isolation gates and fix vacuous setting stub in isolation test

* fix(accounting): address second-wave review findings

* test(accounting): polish review minors

- drop unused imports in cutover spec, use synthetic source id in mapper spec
- add multi-page pagination test for mark-to-market candidate selection
- scope isolation-gate allowlist marker to write blocks only, with self-test

* test(accounting): harden branch coverage to >=90% across module

- add controller, module-compile, DTO, repository and entity-relation tests
  (structural files to 100%)
- cover consumer and service error/fallback/skip branches; module branch
  coverage 81.5% -> 97%, 619 tests
- replace 4 weak tests with real behaviour assertions (fee amount/account,
  dex txId filter, bootstrap idempotency re-run, equity-parity arithmetic)
- fix: openUnattributed passed -0 instead of undefined amountChf for
  needsMark opening legs (found via coverage)

* test(accounting): use Type<unknown> instead of Function in relations spec

Satisfies @typescript-eslint no-unsafe-function-type (CI lint).

* fix(accounting): widen cent columns to bigint and make lazy account creation race-safe

- ledger_leg.amountChfCents / ledger_tx.amountChfSum become bigint: the int4
  ceiling (~21.4m CHF per leg) would permanently stall a source chain on the
  first oversized transfer. A fail-loud transformer converts the PG string
  back to number and throws beyond Number.MAX_SAFE_INTEGER instead of rounding.
- findOrCreate now treats a concurrent UNIQUE(name) violation (23505) as the
  documented no-op by re-fetching the winning row; any other error propagates.

* fix(accounting): stop double-booking card inputs across the cutover boundary

Card buy-crypto rows are excluded from the per-row cutover opening (their seq0
is owned by the forward consumer), and the consumer card gate now blocks
seq0/seq1 for rows whose immutable outputDate shows they had settled at the
cutover snapshot - their value is already inside the aggregate opening. The
cutover snapshot date is pinned as a setting so the gate has a stable anchor.

* fix(accounting): repair transit-age query, arm equity-parity alarm, fix reconciliation units

- checkTransitAge joined only leg.account but selected MIN(leg.bookingDate),
  a column that lives on ledger_tx - every reconciliation run would crash on
  real Postgres. Now joins leg.tx and selects MIN(tx.bookingDate); the spec
  asserts the exact join/select so a regression fails.
- checkEquityParity was log-only. It now alarms (new LEDGER_EQUITY_PARITY mail
  context) above a runtime threshold, compares against the median of the last
  5 valid FinancialDataLog snapshots (immune to transient snapshot skew) and
  folds the open referral-credit liability in explicitly, logging every
  component so the difference stays attributable.
- Per-asset reconciliation compared a NATIVE quantity diff against a
  CHF-denominated tolerance; the diff is now marked to CHF first, and assets
  without a mark are reported as unverified instead of silently passing.
- getEquityComparison required no lower bound and re-read feeds per period;
  from is now mandatory (92-day cap) and the per-day aggregates collapse into
  one grouped query.

* test(accounting): align controller guard assertions with variadic RoleGuard

develop changed RoleGuardClass to hold an entryRoles array (multi-role OR
support); the spec still probed the removed entryRole field.

* fix(accounting): address review findings (consumer wedges, dex idempotency, conventions)

- buy-fiat scans now load fiatOutput.bank.asset - the settlement leg read it
  unloaded and threw on the first tracked-bank row, wedging the consumer
- content-change scans preload marks with a 2-day lookback; the zero-width
  window left every non-CHF row unmarkable and stalled the cursor
- liquidity-order-dex sourceId includes the row id: (context, correlationId)
  is not unique, so a second settled swap was silently dropped
- conventions: English-only comments, PascalCase enum values, logger colon
  format, shared cent transformer, no redundant try/catch in cron runners

* fix(accounting): second review pass (forward-scan lookback, reversal walk, recon relations)

- forward scans preload marks with the same 2-day lookback as the
  content-change path; the earliest batch row previously fell exactly on the
  window start and could never be marked, wedging the consumer
- activeTx walks the reversal chain via the loaded reversalOfId scalar and
  requires the re-book at reversal.seq + 1; the unloaded relation made the
  null-check vacuous and a seq gap could pick a foreign tx as active
- reconcileAssets loads asset.bank so custody classification stops falling
  back to the on-chain default for every account (bank accounts were marked
  stale over weekends and their diff reconciliation silently skipped)
- conventions: import order, section comment style, dead snapshot parameter,
  bank lookup and native-balance aggregation without N+1

* fix(accounting): third review pass (bank-funded completion gate, atomic rebook, dto enums)

- receivedOpened now recognizes bank-funded buy-cryptos (gates on the bank_tx
  opening booking); a post-cutover bank-funded completion was silently skipped
  in the content-change scan and wedged the forward scan
- the content-change callback no longer ignores a gate-blocked completion -
  it throws like buy-fiat so the cursor retries instead of skipping the row
- reverseAndRebookIfChanged wraps reversal and re-book in one transaction;
  a crash between the two left a flat-reversal state that unique-collided on
  every retry
- admin recon status marks the native diff to CHF before the tolerance check,
  matching the reconciliation job; feedless accounts report as unverified
- conventions: DTO status unions become PascalCase enums (endpoints ship with
  this PR, no consumer exists), cutover import order fixed
- test substance: exact spread-leg amounts, real re-run idempotency case,
  discriminating ambiguous-sweep assertions

* fix(accounting): fourth review pass (late-settling coverage, chain rebooks, cutover fail-loud)

- every status-filtered source now has content-change coverage: a row the
  forward watermark skipped while unsettled is forward-booked idempotently
  once it settles (payout, trading, liq-mgmt, dex; crypto-input and
  exchange-tx callbacks now forward-book instead of only reversing)
- gate-blocked rows distinguish never-bookable (settled before the cutover
  snapshot - value lives in the aggregate opening - skip and advance) from
  not-yet-bookable (throw and retry); a flag bump on an old row no longer
  wedges the scan
- value-coupled seq chains reverse and re-book as one interleaved
  transaction, so a late amount change cannot leave the liability open
- cutover openings and owedReferenceRate fail loud on missing marks instead
  of booking silent zeros; dead injection and any-casts removed

* fix(accounting): fifth review pass (owed-straddling cutover boundary, conventions)

- openManualDebt and openBuyFiatOwed abort the cutover run loudly on a
  missing mark instead of silently dropping the CHF value or skipping the
  opening; already-booked openings are skipped idempotently on the retry
- buy-crypto recognizes the cutover owed-opening marker: a row whose owed
  was opened at the cutover skips seq1 (and the card seq0) instead of
  wedging on the received gate or double-counting the card inflow
- conventions: existsBy for existence checks, drop redundant eager:false,
  spell out LedgerBookingLiquidityManagement, move the valid-financial-log
  query into the repository, capitalize client-facing error messages

* fix(accounting): sixth review pass (uniform settled-before-cutover guard, conventions)

- every asset consumer now carries the settled-before-cutover guard, not
  just buy-crypto/buy-fiat: a pre-cutover-settled row re-selected by an
  updated bump can no longer re-book its seq0 on top of the aggregate
  opening (wallet double-count + phantom liability); the cutover persists
  a per-source boundary id as the durable signal
- conventions: English-only comment leftovers, unused logger, capitalized
  plain-prose error messages, NotFoundException for unknown accounts,
  import order, Promise.all for independent reads, single ORed query for
  the unattributed opening

* fix(accounting): seventh review pass (pinned cutover boundary, checkout native units, custody classes)

- the cutover pins boundary ids and hole ids set-once right after pinning
  the snapshot, before any opening: a fail-loud retry hours later can no
  longer widen the settled-classification window and double-book rows whose
  updated was bumped in between; the remaining seconds-wide residual is
  documented instead of claimed impossible
- Checkout custody accounts carry one native convention (gross in card
  currency, CHF mark-consistent): the card seq0 books inputReferenceAmount
  and the CheckoutLtd settlement books gross native, so mark-to-market no
  longer flushes the acquirer fee as phantom fx pnl double-counted in the
  margin report
- custody classification uses the objective blockchain lists (XT/MEXC were
  falling back to on-chain thresholds, frozen bank assets to 24h): BankDead
  maps to OlkyFrozen; the unreachable order-driven/feedless exchange classes
  are removed with their thresholds

* fix(accounting): harden checkout fee ladder and pin-resume semantics

- ladder 2 requires an explicit non-null charge currency matching the
  custody account unit (a null==null match could smuggle an unverified
  unit in); mismatches fall through to the mark bridge
- document that a fee not yet CHF-priced books net-as-gross temporarily
  and self-heals via the content-change rebook once priced
- mid-loop resume test: a crash between boundary write and watermark write
  re-derives the watermark from the pinned boundary, never a fresh MAX(id)

* fix(accounting): eighth review pass (card collateral cutover, mark bridge, stable fill ranking)

- open-at-cutover card rows get a per-row received opening: the Checkout
  collateral feed already carries their gross, so the forward seq0 gates on
  the cutover marker instead of double-booking onto the aggregate opening
- asset openings book under per-asset source ids; a retry after a partial
  crash can no longer shift sequence slots when the chart of accounts grew
  in between
- exchange trade source ids carry the venue prefix and the fill ranking is
  status-agnostic, so an ok-to-failed flip neither collides nor reshuffles
  surviving fills
- mixed transactions with a missing historical mark value the open leg at
  the latest available mark (warn-logged, needsMark kept) or defer the row
  loudly - bookTx never receives an unbalanceable leg set and the head of
  the line can no longer wedge
- fail-loud guards for null trade cost, null output amounts and CHF-derived
  forward fees; isolation gate covers upsert/soft deletes and fails closed
  on an empty scan

* fix(accounting): ninth review pass (route checkout settlement through the plug, transit age)

- checkoutLtdLegs returns through withFxPlug so a missing historical mark
  bridges the two asset legs instead of throwing a CHF imbalance that
  wedged the whole bank_tx forward scan
- the dex network-fee expense leg derives its CHF from the same bridged
  mark as its asset counter-leg, keeping mixed swaps balanced
- transit-age alarms measure since the balance last left zero instead of
  the first booking ever, so churning routes stop reporting fresh residuals
  as ancient

* fix(accounting): tenth review pass (paymentLink cutover, unpriced-row pinning, bridge routing)

- straddling paymentLink rows open per-row on LIABILITY/paymentLink and the
  completion gates accept that marker; they no longer wedge the scan or land
  in the wrong bucket
- rows open and unpriced at the cutover are pinned: the scans skip them with
  an alarm instead of throwing forever, and the card seq0 cannot re-book
  gross that the collateral feed already carried into the aggregate opening
- paymentLink-funded buy-cryptos clear LIABILITY/paymentLink instead of
  letting both liabilities drift equity-neutrally
- the Scrypt trade branch, payment seq0 and payout liability counter route
  through the mark bridge: no leg set reaches bookTx unbalanceable and no
  unvalued leg lands on a CHF-denominated liability

* fix(accounting): eleventh review pass (received opening mutually exclusive with input seq0)

An open buy row whose funding crypto input had not settled at the snapshot
no longer gets a per-row received/paymentLink opening: the input's value is
not in the aggregate asset opening yet, so the forward crypto_input seq0 is
the single opener and the pair would have double-credited the liability.
Settled inputs, card and bank funded rows open per-row as before; a
counter-check confirms the new tests fail without the guard.

* fix(accounting): twelfth review pass (per-row opening keys on the pinned boundary)

The received/paymentLink per-row cutover opening now keys on the same
immutable at-snapshot boundary as the forward seq0 suppression instead of
the mutable live status: an input settling inside the retry window can no
longer be opened per-row AND booked by its forward seq0. The opening is
the exact complement of the suppression by construction.

* fix(accounting): thirteenth review pass (owed opening joins the boundary complement)

openBuyFiatOwed now carries the same pinned-boundary coverage guard as the
received openings, so its paymentLink and owed per-row openings can no
longer pair with a forward seq0 of an input that settled inside the retry
window; stale comments describing the old live-status logic rewritten.

* fix(accounting): fourteenth review pass (fourth opening joins the boundary complement)

openBuyCryptoOwed carries the same pinned-boundary coverage guard as its
three siblings, closing the last opener/suppression divergence cell in the
snapshot-to-pin residual window; the buy-fiat owed relations comment now
documents the coverage read.
…4214)

getChangeLog summed exchange fees for Kraken and Binance only, while the
liquidity flow has been running through Scrypt and MEXC - their trade and
withdrawal fees (sign-aware, rebates included) were missing from the
month-to-date cost side. Both venues now get their own minus blocks, the
dashboard maps them with zero-defaults so historic log entries stay
readable.
TaprootFreak and others added 5 commits July 15, 2026 15:47
…4213)

Allow structured read-only debug queries on the RealUnit share-register
registration state (lifecycle status, forward date, email-confirmation
latch) for production support forensics. PII and secret columns (email,
signature, signedPayload, kycData) stay excluded and are now covered by
the sensitive-column guard test.
…d fail loud on zero-match confirms (#4215)

* fix(realunit): gate buy quotes on the confirmed registration email and fail loud on zero-match confirms

The buy quote only checked that a primary email exists, so a user whose
Aktionariat registration still awaits its email confirmation received a
valid quote and then failed at confirm time with an opaque 400. The quote
now resolves the registration once and derives emailConfirmed from the
same logic as GET /v1/realunit/registration, returning isValid:false with
the new PrimaryEmailNotConfirmed quote error. Grandfathered registrations
keep buying.

The public confirm-aktionariat endpoint reported status 'confirmed' with
an empty confirmedAddresses array when Aktionariat returned 2xx but no
registration matched the link email — a silent fake success. It now
reports 'confirmed_no_registration', logs at error level and writes the
audit row with error severity.

* style(realunit): apply prettier formatting

* docs(realunit): document the confirmed_no_registration state on the confirm endpoint and pin the missing-email precedence

The public confirm-aktionariat API description now lists all four mapped
states. A test pins that a missing primary email takes precedence over an
unconfirmed registration in the quote error.
Total balance moves through transactional yield, price drift on open
positions and errors - the price component was only derivable as a
residual. Every financial data log entry now carries fxPnlChf: the sum of
previous net position times price change per financialType asset since the
previous snapshot. Additive and backward-compatible: the first entry omits
the field, the dashboard maps missing values to zero, the valid-flag chain
and safety mode are untouched.
#4216)

* fix(bank-tx): count bank account fee bookings in the monthly bank cost

getBankTxFee summed chargeAmountChf only, a field the statement import
stopped populating with the bank migration in December 2025 - the monthly
bank cost has read zero ever since while the real fees arrive as dedicated
BankAccountFee rows. Those rows now aggregate per currency (debits as
cost, credits netted) and convert to CHF alongside the legacy per-tx
charge sum; a missing price throws instead of hiding the position.

* fix(bank-tx): address review findings (safety-mode isolation, aggregation, rounding)

- the change log moved behind the equity path and runs in its own
  try/catch: a reporting price failure logs and skips the minute's changes
  entry instead of arming the equity safety mode and suppressing the
  financial data log
- bank account fee rows aggregate server-side (GROUP BY currency and
  indicator) with a new composite index on bank_tx (type, created) instead
  of hydrating full entities every minute
- the CHF total rounds like its sepa-parser sibling; conversion tests use
  distinct non-unit prices per currency so direction and mapping errors
  fail

* test(bank-tx): pass the Frick service in the positional service construction

develop added BankTxFrickService to the constructor; the spec built the
service positionally and fell one argument short.
… break circular dependency (#4220)

AccountingModule (#3880) is now the first module in core.module.ts to
require LiquidityManagementModule, which transitively re-enters
ReferralModule (via BankModule -> UserModule) before finishing its own
export. ReferralModule's own unwrapped LiquidityManagementModule import
then captures undefined, crashing Nest bootstrap ("Nest cannot create
the ReferralModule instance") on every restart in DEV.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants