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
| Fact | Value |
|---|---|
| Script | tools/scripts/migrate-entry-account-shape.mjs (core + tests in tools/scripts/lib / tools/scripts/test) |
| Runs as | an engineer, from a workstation — never from a Worker or CI |
| Selects | lines of adb-accounting.domain_object_accounting_entry whose account has no _id — by shape, never by id |
| Converts with | resolveLedgerAccount(organisationId, accountKey(account)) then toStoredEntryAccount(account, resolved) — the seam's functions, over @adb/data's default ledger reads |
| Writes | updateOne + $set: { account }, guarded by _id, organisationId and the exact old account. No insert, delete, replace, index or transaction |
| Never written | amounts, directions, journals, sequences, transaction ids, dates, settlements, any JVM-written line (_class) |
| Unresolvable line | reported and left as is (the JVM refuses such a posting — gaps.md § FL-DEC2-1c) |
| Log | write-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 guard | refused 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
| Shape | Written by | Stored account | What it misses |
|---|---|---|---|
flat | postRule before #379 (account: e.account) over posting/ledgerAccounts.mjs gl() / auxiliary() | accountNumber, accountNature GENERAL_LEDGER/AUXILIARY, accountType, descriptionKey, target, parentAccountNumber, auxiliaryNumber, currency | accountShort — invisible to the JVM-shaped reads; no ids |
pre-shape | before #379: eventPosting.toStorageAccount (« Saisie manuelle », pre-built events) and initialRentPosting.storageAccount | type GeneralLedgerAccount/AuxiliaryLedgerAccount, an accountShort without ids, flat fields beside | the ledger account ids, qualifier, category, collective |
derived | since #379, entryAccount.derivedDetails when no ledger account exists | accountShort + parent.accountShort derived from the line, flat fields beside | everything 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-dispatchroutes anupdateof an entry toACCOUNTING_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 whosebalanceschange emitsAUXILIARY_BALANCE_UPDATED(routing.mjs:362-385) →services/contractsonAuxiliaryBalanceUpdated(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-modifiedtrigger fires on updates (infra/src/catalog/services.js:58) andAccountingFunctions.onAccountingEntryModifiedruns itsonUpdatebranch,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'snotelines 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 installat the repository root (the script needs themongodbdriver of@adb/scriptsand the workspace links the seam's modules resolve through).- The connection string in
MONGO_URI, exported in a shell that is not logged (--urirefuses 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
toStoredEntryAccountproduces at the time of the run. Run after thelabels.enchange is deployed, or accept French-only labels on the migrated lines, like every line booked before it. parent.balancesof a migrated auxiliary line is the collective's snapshot at migration time, not at posting time; no reader uses it.creationDateTimeis the one-pass value (gaps.md § FL-DEC2-1b).