feat/rpu-history
needs attentionviewing older commitc45865d · fullPR #324reviewed 2026-07-22 19:15 UTC0H · 3M · 4L · 4I- Purpose
- Complete the RPU-agreements UX arc: surface per-chapter regime history, fix per-bill tariff display, and refresh stale titular data from newer bill evidence
- Goal
- §3 of rpu-agreements-model.md — rate-tag chip, per-bill regime in Historial, RPU-union bill count, regime-timeline drawer, evidence display refresh
- Sub-goals
- SG-1: Rate-tag chip (tariff_code/utility_service_code columns on UCO + chip component)
- SG-2: Per-bill regime (extractBillOwnTariff: TARIFA_REG wins over contract tariff)
- SG-3: RPU-union bill count (COUNT across all chapters sharing contract_number)
- SG-4: Regime-timeline drawer (ContractDetailDrawer + GET /chapters endpoint)
- SG-5: RPU-complete exports — already correct, no change
- SG-6: Evidence display refresh (UseExistingContract.refresh + OCC updateWithVersion)
- What
- 37 files, +15817/−112. Migration 0063 adds tariff_code/utility_service_code to utility_contract_overview and rewrites bill_count to the RPU union. New chapters endpoint. New ContractDetailDrawer. extractBillOwnTariff pure function. Display refresh in EnsureContractFromBills shell.
- Why
- Closes the last branch of the RPU-agreements arc (#314/#319/#321/#323 already on main). Fixes the stale-titular bug and surfaces seasonal tariff oscillation (DB1↔DB2) per bill row.
- Areas
- domains/utility/src/utility-contract+356−20domains/utility/src/utility-contract-overview+269−0domains/utility/src/bill+124−14apps/platform/src/app/[locale]/(dashboard)/bills+507−95apps/platform/src/api+135−0packages/api/src+69−0packages/database/drizzle+516−0packages/database/src/schema+8−0docs/development+324−0
- Blast
- 37 files, +15817/−112. Touches utility domain decisions/shells/queries, UCO read model (migration + triggers), platform UI (Contratos table, Historial page, new drawer), API contract layer, and DB schema.
Findings · 11
correctness3
OCC race on display refresh defers silently — by design, documented in PR
domains/utility/src/utility-contract/utility-contract.shells.ts
When updateWithVersion returns null (lost OCC race), the shell returns the pre-refresh contract with action='found' and logs 'deferred'. The refresh only re-fires when the next bill has a strictly-newer periodEnd. The log says 'deferred' but nothing actively re-queues — it is naturally re-triggered on the next ingest cycle. Documented as intentional best-effort behaviour; no action required.
conceptName literals 'tariff'/'regulatedTariff' confirmed valid — correctness finding retracted
domains/utility/src/bill/bill.decisions.ts
The review panel initially flagged extractBillOwnTariff's use of camelCase conceptName literals. Verified: EnrichedLineItem carries a conceptName field populated from the bill_concepts_catalog, and 'tariff'/'regulatedTariff' are confirmed catalog conceptName values. The function is correct.
Trigger fan-out is atomic — TOCTOU concern retracted
packages/database/drizzle/0063_youthful_martin_li.sql
The uco_on_bills_* triggers call utility_contract_overview_refresh() per affected (org, contract), which internally calls derive() — a full atomic re-derive + upsert. bill_count is recomputed as a LATERAL COUNT(*) subquery, not a read-then-increment. No race possible.
security1
chapters query uses bare database after org-gate — no org-id predicate on chapter rows
apps/platform/src/api/handlers/utility-contracts.handler.ts
After the RLS-enforced overview lookup establishes org ownership, the subsequent utility_contracts query runs against plain `database` filtering only on contract_number + deleted_at. A contract_number collision between two orgs (data-entry error, multi-tenant edge case) would return the other org's chapters. Adding `.where(eq(utilityContracts.orgId, orgId))` (or scoping through the rlsDb transaction) eliminates the risk structurally.
conventions2
listContractChaptersHandler throws instead of returning discriminated error
apps/platform/src/api/handlers/utility-contracts.handler.ts
throw new Error('Request object is required') deviates from the FCIS convention of never throwing; it would surface as an unhandled 500 if the request object is ever absent in a test harness or future call site. This is the established file-wide pattern for all handlers here, so it is consistent within the file — but the file-level pattern is itself a deviation.
ContractChapterItem.status typed as string rather than CONTRACT_STATUSES enum
packages/api/src/schemas/utility-contract.schemas.ts
ContractChapterItemSchema uses z.string() for status, consistent with UtilityContractResponse.status elsewhere in this file (widened at API boundary). A z.enum(CONTRACT_STATUSES) would give callers type-safe discrimination. Low risk since isCurrent is pre-computed by the handler.
tests3
chapters endpoint (GET /utility-contracts/:publicId/chapters) has zero test coverage
apps/platform/src/api/handlers/utility-contracts.handler.ts
listContractChaptersHandler has no handler-level or integration test. The UCO integration suite covers the underlying DB query but never exercises this HTTP route or its response shape. New billing-visible endpoints warrant at least a happy-path smoke test.
getBillPeriodRangesByContracts has no tests
domains/utility/src/bill/bill.queries.ts
New query function with COUNT/MIN/MAX aggregation across the chapter union — zero test coverage (confirmed by grep). Warrants an integration test covering the union semantics (bills on sibling chapters counted together).
RateTagChip and ContractDetailDrawer have no component tests
apps/platform/src/app/[locale]/(dashboard)/bills/_components/RateTagChip.tsx
Two new user-facing components (RateTagChip, ContractDetailDrawer) have no render or snapshot tests. At minimum, a test covering the null/empty tariff code path and a basic render of the drawer with one active chapter would guard against regressions.
improvement2
LEGACY_TARIFF_DISPLAY_MAP duplicated from xml-parser — silent drift risk
domains/utility/src/bill/bill.decisions.ts
The comment explicitly says 'Mirror of the xml-parser's LEGACY_TARIFF_MAP — keep in sync'. Two hand-maintained copies will diverge silently when a new legacy code is added. Extracting to a shared package (e.g. @batu/api or a utility-bill-concepts constants file) or importing from the xml-parser's constants would eliminate this.
window.history.replaceState less idiomatic than useRouter().replace() in App Router
apps/platform/src/app/[locale]/(dashboard)/bills/contratos/_components/ContractDetailDrawer.tsx
Direct replaceState bypasses Next.js App Router's internal state (prefetch cache, scroll restoration, transition state). useRouter().replace(url, { scroll: false }) achieves the same shallow URL update while keeping the router in sync. Low impact today but may cause subtle issues if searchParams-keyed data fetching is added later.