fix/job-active
needs attentionviewing older commitd4a8d84 · fullPR #314reviewed 2026-07-16 00:04 UTC0H · 3M · 5L · 3I- Purpose
- Fix production incident (2026-07-14, Solfium RPU 077120901845): status-blind findByContractNumber picked a terminated history row over the active one, minting a junk site named after the RPU and a corpse SUC against it
- Goal
- Implement the RPU agreements model (design doc approved by Diego 2026-07-15): RPU = service point, contract rows = regime agreements, exactly one active per RPU. All resolution paths use active-first; future inversions self-heal via evidence-driven reactivation
- Sub-goals
- SG-1: findByContractNumberActiveFirst — adopt in job-intent shell (single+batch), cfe-jobs handler (create/retry/batch), public-v1 handler, wizard PATH-2 linking, payment-status fallback writers
- SG-2: Terminated-only RPU guard — no site/SUC/subscriptions minted; job still dispatches; persist layer creates them when real bills arrive
- SG-3: ReactivateContract decision + shell — strictly-newer RPU-wide evidence reactivates terminated match; demotes conflicting active row + re-points attachments atomically
- SG-4: findNewestXmlPeriodEndForRpu — RPU-wide XML max for reactivation guard (forceRefresh re-downloads never flip status)
- SG-5: loadPersistBatchContext fix — activeContractForRpu correctly finds active row OTHER than compound match (was null when compound match existed)
- What
- 18 files across utility domain, cross-domain, 3 platform handlers, 1 lambda, and docs. Core: new ReactivateContract decision/shell branch, findByContractNumberActiveFirst query, 3 deduped reassign functions, RPU-wide bill query. No schema changes; no API wire-type reshapes; CommitCfeJobIntentResult.site/siteUtilityContract nullable (consumers verified).
- Why
- Live production incident + Diego's functionality-first sequencing decision (this merges before repair waves PR #306). Evidence-driven self-heal eliminates need for future repair scripts on this class of inversion.
- Areas
- domains/utility/src/utility-contract+345−6domains/utility/src/bill+32−4domains/utility/src/monitoring-subscription+39−0domains/utility/src/payment-monitoring-subscription+39−0domains/utility/src/site-utility-contract+43−0domains/cross-domain/src+240−15apps/platform/src/api/handlers+19−5services/utility/bills/cfe/src/handlers+3−1docs/development+205−0
- Blast
- 18 files, +925/−31 (excluding doc). Blast center: utility-contract.shells.ts (new ReactivateContract branch), cfe-job-intent.shells.ts (terminated guard), bill.queries.ts (RPU-wide query). No schema migration, no API contract reshape, no wire-type change.
Findings · 11
correctness1
Demotion updateWithVersion return unchecked — silent two-active-contracts race
domains/utility/src/utility-contract/utility-contract.shells.ts:864
The reactivation leg correctly gates: `if (!reactivated) return err(ContractErrors.databaseError('update'))`. The immediately following demotion — `await contractQueries.updateWithVersion(tx, demoted.id, { status: 'terminated' }, demoted.version)` — does not capture or check the return. updateWithVersion returns null when the WHERE id=... matches 0 rows (concurrent hard-delete). If demotion silently fails, the code still emits the 'superseded_by_reactivation' outbox event and proceeds to reassign SUCs/subscriptions from demoted.id to reactivated.id — leaving the RPU with two active contracts, one now orphaned from its attachments. The TerminateAndReplace branch has the same pre-existing gap; this PR adds another load-bearing instance. Fix: `const demotedResult = await contractQueries.updateWithVersion(tx, demoted.id, { status: 'terminated' }, demoted.version); if (!demotedResult) return err(ContractErrors.databaseError('update'));` before the outbox insert.
conventions2
No exhaustive switch on EnsureContractFromBillsResult.action — new members are invisible to type-checker at call sites
domains/utility/src/bill/bill.shells.ts:849
The action union is consumed via a series of if-guards, not an exhaustive switch. TypeScript won't catch a future caller that needs to handle 'reactivated' differently. The new member falls through the 'found' guard (siteLinksNeeded = true, fresh query) which is correct, but the absence of a compile-time gate means the semantics were verified by reading the code, not by the type system. Consider a switch with a `satisfies never` default on the action field in the callers, or a type-level constraint at the call site.
findByContractNumberActiveFirst name describes internal algorithm rather than semantic result
domains/utility/src/utility-contract/utility-contract.queries.ts:470
canonical-form.md convention: query names are findBy{Field} (the lookup key) or findActive{Entity}. 'ActiveFirst' describes the internal tiebreaking strategy, not the semantic output ('the canonical contract for this RPU'). A name like findCanonicalByContractNumber would be more idiomatic. Minor — the current name is clear and the JSDoc is thorough. Worth noting before it becomes the template for sibling queries.
tests5
paymentSubscriptionQueries.reassignContractIdDeduped has zero test coverage
domains/utility/src/payment-monitoring-subscription/payment-monitoring-subscription.queries.ts:341
The new deduped variant (DELETE conflicting rows, then UPDATE remaining) is distinct from the pre-existing reassignContractId. The existing payment-monitoring-subscription integration tests cover only reassignContractId (non-deduped). The intent test at cfe-job-intent.integration.test.ts covers subscriptionQueries.reassignContractIdDeduped (monitoring subs) but not the payment variant. A bug in the DELETE subquery's inArray condition for the payment table would go entirely undetected. This function runs in the ReactivateContract+demote path — the higher-risk path for the repair-wave sequencing.
findByContractNumberActiveFirst fallback branch (all-terminated → newest) not directly tested
domains/utility/src/utility-contract/utility-contract.queries.ts:470
The function is exercised only indirectly through the intent shell integration test ('resolves the ACTIVE regime row'). Branch 1 (return active row) is exercised, but branch 2 (all terminated → DESC createdAt LIMIT 1 fallback) is only covered incidentally. There is no test confirming: (a) DESC createdAt ordering picks the newest chapter, (b) soft-deleted rows are excluded from the fallback, (c) null is returned when the RPU has no non-deleted rows. This query replaced status-blind findByContractNumber in 4 handler/lambda locations — a regression in the ordering silently restores the original bug.
Demotion integration test does not verify SUC/subscription re-pointing through the shell
domains/utility/src/utility-contract/__tests__/ensure-contract-evidence.integration.test.ts:358
The 'demotes the different-regime active row on a reactivation swap' test checks demoted contract status and the outbox event, but the demoted contract has zero SUC or subscription rows — so the reassignContractIdDeduped calls are no-ops and the re-pointing logic is not exercised end-to-end. The intent test covers the dedup functions in isolation (cfe-job-intent.integration.test.ts line 873), but not wired through ensureContractFromBillsShell. A bug in any of the three deduped calls inside the ReactivateContract branch would pass all current tests.
commitCfeJobIntentBatchShell terminated-only guard untested at batch level
domains/cross-domain/src/__tests__/cfe-job-intent.integration.test.ts
The single-shell terminated-only path is covered (line 826: 'terminated-only RPU: dispatch context returned but NO site/SUC/subscriptions minted'). The batch shell has the same guard (cfe-job-intent.shells.ts ~line 365) with a continue-inside-loop pattern, but no batch-level test verifies: (a) a mixed batch where some RPUs are terminated-only and others are active creates artifacts only for the active ones, (b) all RPUs appear in the returned Map. The batch path is used in the bulk job creation flow.
findNewestXmlPeriodEndForRpu lacks a direct query-level test
domains/utility/src/bill/bill.queries.ts:1202
Exercised only indirectly through the reactivation shell tests. Unlike the single-contract findNewestXmlPeriodEnd (which has direct tests for null, non-xml exclusion, and max logic), the RPU-scoped variant adds a JOIN through utility_contracts filtered by contractNumber. No direct test verifies: (1) aggregation across multiple contracts for the same RPU, (2) exclusion of soft-deleted contracts, (3) exclusion of non-xml bills at the RPU scope.
improvement3
ensureContractForRpu fetches all rows to find newest terminated — single DESC query would suffice
domains/cross-domain/src/cfe-job-intent.shells.ts:514
When all chapters are terminated, the function calls findAllByContractNumber (fetches all, ASC) and takes all[all.length - 1]. findByContractNumberActiveFirst already encapsulates this logic (active first, falls back to DESC createdAt LIMIT 1). ensureContractForRpu could call findByContractNumberActiveFirst and derive isTerminated from result.status === 'terminated', eliminating the full-table fetch and the conceptual duplication.
Dynamic imports for subscription queries in ReactivateContract branch lack circular-dep justification
domains/utility/src/utility-contract/utility-contract.shells.ts:887
The await import('../monitoring-subscription/...') pattern is copied from the pre-existing TerminateAndReplace branch. monitoring-subscription.queries.ts does not import from utility-contract (confirmed: it imports only schema types from @batu/database), so there is no circular dependency to avoid. siteContractQueries — same role, same module tree — is imported statically at the top of the file with no issues. These can be converted to static top-level imports, eliminating the per-call dynamic resolution and making the import graph consistent with the rest of the file.
Active-first call-site comments slightly redundant across 5 locations
apps/platform/src/api/handlers/cfe-jobs.handler.ts:144
Each of the 5 call sites where findByContractNumber was replaced has a multi-line comment re-explaining the active-first rationale. The WHY is non-obvious and the first comment earns its place (CLAUDE.md: add when WHY is non-obvious). The 4 subsequent instances could be shortened to a single-line pointer: '// active-first — see rpu-agreements-model.md §2.2' since the canonical explanation already lives in the query JSDoc at utility-contract.queries.ts.
History · 6 commits
- 3e1361aneeds attentionincremental0H · 1M · 0L2026-07-16 01:14
- c7b4e31needs attentionincremental0H · 1M · 5L2026-07-16 01:03
- 2099ea7needs attentionincremental0H · 3M · 5L2026-07-16 00:53
- ce2dc3eneeds attentionincremental0H · 1M · 1L2026-07-16 00:44
- df0ef09needs attentionincremental0H · 2M · 3L2026-07-16 00:23
- d4a8d84needs attentionfull0H · 3M · 5L2026-07-16 00:04current