Life ConnectLife Connect
Wiki index
Architecture
Services
UI
Concepts
Runbooks
Infra
Swagger Docs
GitHub
Wiki index
Architecture
Services
UI
Concepts
Runbooks
Infra
Swagger Docs
GitHub
  • Runbooks

    • Run the local stack
    • Deploy a PR preview
    • Provision a new environment
Last updated 2026-10-05

Migrate Cloudflare ledger lines to the JVM account shape

AI-generated content

This document was generated by an AI assistant. Verify accuracy before relying on the details.

Since PR #379 every Cloudflare posting stores, on each ledger line, the account the JVM stores — its AccountingEntryAccountDetails (services/accounting/src/posting/entryAccount.mjs, called at the storage seam postingService.toEntryDocuments). Lines written before that carry a flat postingRules account or an id-less pre-shape, and #379 itself writes an id-less derived shape when no ledger account exists. Flat lines are invisible to every read that filters on account.accountShort.* (statement, settlement pickers, general ledger, journals); no old-shaped line has the account ids the balance refresh keys on. tools/scripts/migrate-entry-account-shape.mjs rewrites each such account with the seam's own resolver — no second implementation — and touches nothing else. Dry-run by default, refuses a production-looking cluster, one guarded $set per line, idempotent, write-ahead rollback log. Greg's decision 4a of 2026-10-05 (gaps.md § FL-DEC2-1d); --apply runs only after his go.

At a glance

FactValue
Scripttools/scripts/migrate-entry-account-shape.mjs (core + tests in tools/scripts/lib / tools/scripts/test)
Runs asan engineer, from a workstation — never from a Worker or CI
Selectslines of adb-accounting.domain_object_accounting_entry whose account has no _id — by shape, never by id
Converts withresolveLedgerAccount(organisationId, accountKey(account)) then toStoredEntryAccount(account, resolved) — the seam's functions, over @adb/data's default ledger reads
WritesupdateOne + $set: { account }, guarded by _id, organisationId and the exact old account. No insert, delete, replace, index or transaction
Never writtenamounts, directions, journals, sequences, transaction ids, dates, settlements, any JVM-written line (_class)
Unresolvable linereported and left as is (the JVM refuses such a posting — gaps.md § FL-DEC2-1c)
Logwrite-ahead JSON Lines, --log <path> (default ./migrate-entry-account-shape-<timestamp>.jsonl)
Rollback--rollback <log> (dry-run) then --rollback <log> --apply — restores the stored bytes
Production guardrefused unless --i-know-this-is-prod; a host is non-production only if local or named like one
Staging, 2026-10-05 (dry-run)6 046 lines, 15 Cloudflare lines, 14 already in the JVM shape, 1 candidate, 0 to rewrite, 1 reported

Details

The three old shapes

ShapeWritten byStored accountWhat it misses
flatpostRule before #379 (account: e.account) over posting/ledgerAccounts.mjs gl() / auxiliary()accountNumber, accountNature GENERAL_LEDGER/AUXILIARY, accountType, descriptionKey, target, parentAccountNumber, auxiliaryNumber, currencyaccountShort — invisible to the JVM-shaped reads; no ids
pre-shapebefore #379: eventPosting.toStorageAccount (« Saisie manuelle », pre-built events) and initialRentPosting.storageAccounttype GeneralLedgerAccount/AuxiliaryLedgerAccount, an accountShort without ids, flat fields besidethe ledger account ids, qualifier, category, collective
derivedsince #379, entryAccount.derivedDetails when no ledger account existsaccountShort + parent.accountShort derived from the line, flat fields besideeverything only the ledger account knows

Each line is converted exactly as the seam converts a line booked today: the ledger account is resolved by LedgerAccountHandler parity (general ledger by the organisation's own chart and account number; auxiliary by organisation and target, with its collective), and the stored account is the JVM's, plus the flat fields the FEC export reads (FLAT_KEYS), as the old account carried them. A test proves that a migrated flat line stores byte for byte what toEntryDocuments stores for the same line today.

Reported, never written: a line whose ledger account does not exist (no-ledger-account, including a general-ledger target that is not the line's own organisation), a JVM-written line (jvm-written), an account that is not a document, a line without organisation, an unknown nature, an account holding a value canonical Extended JSON could not restore byte for byte (a JS number, undefined, an integer-like key — round-trip-hazard), and two defensive cases (a ledger account of another organisation, a conversion without ids). A line with settlements not transferred is planned but carries a note (§ Side effects).

What the dry-run found on staging (2026-10-05, read-only)

The single candidate is the derived line 6ac3b2420e80e5e5c10a1262 (OUTGOING_AGENCY_INVOICE, organisation 6ac3acddf31bc3cf959eb846): the agency self-supplier SP-00000000 (PersonCompany 6ac3acdebb3ebb32cd96006f) has no auxiliary account — reported. The 3 flat and 15 pre-shape lines named in gaps.md § FL-DEC2-1d are no longer on staging: their organisation 6ac368aa0d6a8ae518de3843 has no entry, event, ledger account or adb-persons.organization row any more (an end-to-end organisation that was cleaned up). Every other Cloudflare line on staging was written after #379 and is already in the JVM shape.

Side effects of the writes

Every write is a $set, so each emits one change event of type update (a test asserts no replace).

  • Cloudflare (staging). services/event-dispatch routes an update of an entry to ACCOUNTING_ENTRY_MODIFIED (services/event-dispatch/src/routing.mjs:511-526) → onAccountingEntryModified (services/accounting/src/consumers/consumer.mjs:316-352) → refreshBalancesForEntry (services/accounting/src/posting/balanceRefresh.mjs:203-252): the balance snapshot of the line's ledger account (and of its collective) is recomputed by the full aggregation — idempotent, and it now includes the line, as it would have had the line been booked in the JVM shape. An auxiliary whose balances change emits AUXILIARY_BALANCE_UPDATED (routing.mjs:362-385) → services/contracts onAuxiliaryBalanceUpdated (services/contracts/src/consumer.mjs:453), the tenant dunning evaluation. Before the migration the refresh skipped these lines (no account id, logged).
  • JVM (production topology). The accounting-entry-modified trigger fires on updates (infra/src/catalog/services.js:58) and AccountingFunctions.onAccountingEntryModified runs its onUpdate branch, postSettlementCallback (AccountingFunctions.java:132-151, :202-231): post-settlement transfers for a DEBIT line of an allowed event carrying settlements not transferred. Production holds no Cloudflare line, so its dry-run must show 0 candidates. Where a JVM listener receives a cluster's entry updates, the plan's note lines name the candidates that would reach that branch.

Prerequisites

  • Greg has read a dry-run of the target cluster.
  • A mongodump of adb-accounting.domain_object_accounting_entry, taken right before --apply.
  • pnpm install at the repository root (the script needs the mongodb driver of @adb/scripts and the workspace links the seam's modules resolve through).
  • The connection string in MONGO_URI, exported in a shell that is not logged (--uri refuses credentials).

Steps — staging

export MONGO_URI='mongodb+srv://…@adb-stage.cm0co.mongodb.net/'
node tools/scripts/migrate-entry-account-shape.mjs | tee stage-dry-run.txt          # read the header: the cluster, non-production
node tools/scripts/migrate-entry-account-shape.mjs --verbose                         # each line's account before / after
mongodump --uri "$MONGO_URI" --db adb-accounting --collection domain_object_accounting_entry --out "backup-$(date +%Y%m%d)"
node tools/scripts/migrate-entry-account-shape.mjs --apply --limit 1 --log stage-entry-account.jsonl
node tools/scripts/migrate-entry-account-shape.mjs --apply --log stage-entry-account.jsonl
node tools/scripts/migrate-entry-account-shape.mjs                                   # verify: to rewrite 0, only REPORTs remain

Per line: PLAN (or REPORT with its category and reason) names the line, its transaction and event type, and the ledger account it resolved. Under --apply the intent (before + after) is appended to the log, then the guarded $set, then its result; a line changed since it was read is SKIPPED and logged as such. Re-run the same command after an interruption: done lines no longer match. Then check a migrated payment in the statement and in « Sélectionner un règlement ». Keep the log with the backup.

Steps — production

Cloudflare does not post in production, so the dry-run there is the whole job: it must show 0 candidates.

node tools/scripts/migrate-entry-account-shape.mjs --i-know-this-is-prod | tee prod-dry-run.txt

If it does not, stop and escalate — do not apply (§ Side effects, the JVM's onUpdate branch).

Rollback

node tools/scripts/migrate-entry-account-shape.mjs --rollback stage-entry-account.jsonl          # what it would restore
node tools/scripts/migrate-entry-account-shape.mjs --rollback stage-entry-account.jsonl --apply

Newest entry first, each restore is a $set of the old account guarded by the line's _id, organisation and migrated account: a line changed since the migration is left alone (not-in-migrated-state), an entry whose own write did not land is skipped, and an entry without a result line is attempted and counted uncertain. A second rollback restores nothing. The migrated account depends on the ledger documents, so a log entry is checked structurally before anything is restored: old shape before, JVM shape after, the same ledger account on both sides, the same flat fields, the line's own organisation — a hand-edited log cannot restore an account naming another ledger account or tenant. The rollback refuses a log recorded against another cluster, and the same production guard applies. Restoring re-triggers the balance refresh (§ Side effects), which then skips the line again.

Open questions

  • The reported staging line (6ac3b2420e80e5e5c10a1262): it stays derived until the agency's auxiliary account exists (then a re-run migrates it) or a decision removes the posting. Once the posting engine refuses a posting without a ledger account (gaps.md § FL-DEC2-1c), no new derived line appears.
  • Catalog labels (gaps.md § FL-DEC2-1a): the migrated accounts carry what toStoredEntryAccount produces at the time of the run. Run after the labels.en change is deployed, or accept French-only labels on the migrated lines, like every line booked before it.
  • parent.balances of a migrated auxiliary line is the collective's snapshot at migration time, not at posting time; no reader uses it. creationDateTime is the one-pass value (gaps.md § FL-DEC2-1b).
Edit this page
Last Updated:
Contributors: Grégory Horion