← all branches

fix/cfe-inputs

needs attentionviewing older commit
6e0ff6f · fullPR #287reviewed 2026-07-09 17:17 UTC1H · 5M · 5L · 3I
The branch
Purpose
CFE collect pipeline silently discarded two caller-supplied inputs (service_name + latest_total), deadlocking name-broken RPUs whose stored bills are stale. Confirmed live on Tiendas Neto RPU 144210900626.
Goal
Honor caller-supplied service_name and latest_total throughout the collect pipeline; add a confidence gate on stored bill totals; introduce LATEST_TOTAL_REQUIRED as a non-retryable user-actionable error code; add public API parity for these inputs on POST /v1/jobs.
Sub-goals
  • SG-1: decideCacheStatus honors caller latestTotal (wins over stored bill); confidence gate rejects stale stored totals
  • SG-2: cacheCheckShell plumbs latestTotal from job.config through to decideCacheStatus
  • SG-3: collector.lambda.ts surfaces LATEST_TOTAL_REQUIRED (non-retryable) instead of futile SFN retry when no trusted total is available
  • SG-4: POST /v1/jobs accepts optional latest_total; service_name for existing contracts now persists via updateContractShell (API-first parity)
The changes (whole branch)
What
15 files, +203/-12 across domains/utility (decisions + shell), services/utility/bills/cfe (error codes + collector), apps/platform (handler, validation, UI error map, i18n), packages/api (schema + types). Additive and backward-compatible: all new fields optional, no wire-type reshape.
Why
RPUs with a stale stored bill total and a rejected service name were deadlocked — registration needs a current total to accept the corrected name, but the only way to get a current total is to register. The caller (UI or API) can supply the latest bill total to break the chicken-and-egg.
Areas
domains/utility/src/cfe-job+733services/utility/bills/cfe+578apps/platform/src/api+561apps/platform/src/app+90apps/platform/src/messages+20packages/api/src+20
Blast
15 files, +203/-12 lines. CFE collect pipeline (decision + shell + lambda), public-v1 jobs handler, validation, UI error mapping, i18n strings. No schema migrations, no new DB tables, no CDK/infra changes. Backward-compatible.
non-atomic-writes direct-query-in-handler
typecheck· @batu/utility-domain, @batu/utility-bills-cfe, @batu/platform all typecheck clean (per PR description)unit-tests· cfe-job.decisions 102/102; CFE errors+collector 32/32 (per PR description)ci· No CI check data available from gh pr checks

Findings · 14

correctness3

high

updateContractShell commits before createJobShell — partial write if job creation fails

apps/platform/src/api/handlers/public-v1/jobs.handler.ts:114

updateContractShell at ~line 114 runs in its own transaction and commits before createJobShell (~line 161). If job creation subsequently fails (e.g. JobAlreadyExists, secret-count guard → 500, DB error), the contract serviceName is permanently updated with no corresponding job. The caller sees a 500 with the service name already changed. FCIS (ADR-016) requires both writes to share one transaction boundary or be folded into a single shell. The service-name correction should be part of the job-creation shell, not a fire-and-forget pre-step.

medium

Double RPU lookup — org-scoped read vs unscoped downstream read may return different contracts

apps/platform/src/api/handlers/public-v1/jobs.handler.ts:145

findByRpuAndOrgId (org-scoped, new, ~line 108) and findByContractNumber (unscoped, existing, ~line 145) both query utility_contracts for the same RPU in the same request. If the same RPU appears in multiple orgs (shared-resource / ownership-transfer scenario allowed by the domain model), findByContractNumber may return a different org's contract, causing the wrong existingContractServiceName to flow into runPostCreateChoreography. Consolidate: cache the findByRpuAndOrgId result and pass it to runPostCreateChoreography instead of the unscoped query.

low

LATEST_TOTAL_REQUIRED not in USER_INPUT_ERRORS — future CfeError throw would trigger SFN retry instead of terminal failure

services/utility/bills/cfe/src/handlers/collector.lambda.ts

USER_INPUT_ERRORS contains RPU_TOTAL_MISMATCH but not LATEST_TOTAL_REQUIRED. Both are non-retryable. Today LATEST_TOTAL_REQUIRED is only returned via the explicit fail() path, not thrown as a CfeError, so there is no runtime impact. But a future code path that throws CfeError(LATEST_TOTAL_REQUIRED) would not be caught by the USER_INPUT_ERRORS guard and would trigger a futile SFN retry. Defensive fix: add LATEST_TOTAL_REQUIRED to USER_INPUT_ERRORS.

security3

medium

TOCTOU between findByRpuAndOrgId read and updateContractShell write → opaque 500 on version conflict

apps/platform/src/api/handlers/public-v1/jobs.handler.ts

Between the findByRpuAndOrgId read (~line 108) and the updateContractShell write (~line 114), a concurrent request can bump the contract version. The optimistic-lock in updateContractShell will catch this and return a VersionConflict, but the handler maps that to InternalError → HTTP 500, which is opaque to the caller. No cross-org leak (the lookup is correctly org-scoped), but the error surface is misleading. Consider returning 409 Conflict for VersionConflict, or folding both operations into a single atomic shell.

info

latest_total has no upper bound — Number.MAX_VALUE passes validation

apps/platform/src/api/utils/public-v1-validation.ts:398

isFinite(Number.MAX_VALUE) is true; n >= 0 is true. A caller can submit 1.7e308 as the bill total. CFE's form would reject it (RPU_TOTAL_MISMATCH), not a security risk. A practical upper bound (e.g. 10_000_000 MXN) would produce a cleaner 400 and guard against absurd inputs.

info

Authorization model is correctly org-scoped — no cross-org escalation path

apps/platform/src/api/handlers/public-v1/jobs.handler.ts

findByRpuAndOrgId scopes the query to the caller's orgId via parameterized JOIN through siteUtilityContracts → sites WHERE org_id = orgId. The returned publicId can only belong to a contract in the caller's org. updateContractShell does not re-verify orgId, but the auth gate is the lookup itself. Actor is integrationActor(orgId) — correct org-as-actor pattern. No cross-org escalation found.

conventions2

medium

Handler calls query directly (findByRpuAndOrgId) without admin-required annotation

apps/platform/src/api/handlers/public-v1/jobs.handler.ts:108

ADR-016 / api-patterns.md: handlers never call queries directly — always through shells. The handler already annotates its other direct queries with '// admin-required: pre-shell context lookup' to document the exception. This new call lacks that annotation and also triggers a mutation (updateContractShell), going beyond the read-only exception pattern. Canonical fix: an updateContractServiceNameIfChanged(db, rpu, orgId, serviceName, actor) shell that does the read+decide+write atomically.

info

LATEST_TOTAL_REQUIRED not surfaced on the public API error surface

apps/platform/src/app/[locale]/(dashboard)/bills/descargas/_lib/job-errors.ts

LATEST_TOTAL_REQUIRED is mapped in the platform UI (job-errors.ts + i18n strings) but has no entry in PublicApiErrorCode or mapPublicJobError. A public API consumer polling a job that terminates with LATEST_TOTAL_REQUIRED would see a generic error rather than the actionable code. Whether to expose it on the public API surface is a product decision — if the intent is to guide API callers to re-POST with latest_total, adding the code to the public error schema would close the loop.

tests4

medium

No test for the trustworthiness boundary (bill sitting at the último/penúltimo edge)

domains/utility/src/cfe-job/__tests__/cfe-job.decisions.test.ts

The confidence gate uses: periodEnd + 2×periodLength > today. Tests use staleBill (clearly old) and currentBill (clearly fresh). There is no test for a bill sitting exactly at the penúltimo boundary — one period-length past periodEnd. An off-by-one here means the wrong total is passed to CFE registration, burning an attempt on a stale number.

medium

collector.lambda.ts LATEST_TOTAL_REQUIRED terminal path has no behavioral test

services/utility/bills/cfe/__tests__/unit/domain/errors.test.ts

The new needsLatestTotal → return fail(CfeErrorCode.LATEST_TOTAL_REQUIRED) branch at collector.lambda.ts ~line 197 is not exercised by any handler-level test. The errors.test.ts only verifies the code is in the enum. A test that mocks discoverMiEspacio to return { type: 'recoverable', needsLatestTotal: true } and asserts the collector returns LATEST_TOTAL_REQUIRED would confirm the wiring.

low

latest_total parsing branches not covered in public-v1-validation tests

apps/platform/src/api/utils/public-v1-validation.ts:391

parseJobCreateBody tests cover rpu, type, period_count, monitor, force_refresh but not latest_total. The three parsing branches (number passthrough, string coercion, rejection) and the boundary cases (n=0, n<0, 'abc', '38355.50') are unexercised. The logic is simple but follows the pattern of testing each parsed field.

low

cacheCheckShell plumbing (job.config.latestTotal → decideCacheStatus) has no integration test

domains/utility/src/cfe-job/cfe-job.shells.ts:874

The shell change connects the public API input to the pure decision. No integration test exercises cacheCheckShell with a non-null job.config.latestTotal. A bug in the field name or JSON column path would silently revert to the derived total without any test catching it.

improvement2

low

Trustworthiness formula should name the 2× multiplier (último/penúltimo CFE rule)

domains/utility/src/cfe-job/cfe-job.decisions.ts:956

The expression periodEnd + 2*(periodEnd - periodStart) > today encodes 'último o penúltimo' — CFE accepts either the most recent or the second-most-recent bill total. The 2× factor is non-obvious without that domain knowledge. Extract named variables: const periodLengthMs = periodEnd - periodStart; const twoPeriodsAfterEnd = periodEnd + 2 * periodLengthMs; and add a comment naming the CFE rule. The existing comment block describes WHAT the gate does but not WHY the multiplier is 2.

low

needsLatestTotal boolean flag could be a cleaner union variant

services/utility/bills/cfe/src/handlers/collector.lambda.ts:467

MiDiscoveryResult's recoverable variant carries optional code? and needsLatestTotal? to guide caller branching. This creates implicit sub-variants requiring ordered if/if guards. A dedicated { type: 'needs_latest_total'; message } variant would be type-safe and self-documenting. At minimum, remove the ? optionality from code/needsLatestTotal on the TOTAL_MISMATCH path (narrow via a union or overload) so TypeScript enforces the invariant.

History · 4 commits

  1. d1ec070needs attentionincremental1H · 3M · 4L2026-07-24 00:31
  2. e367b60needs attentionincremental0H · 2M · 2L2026-07-09 18:25
  3. 6f7954aneeds attentionincremental1H · 5M · 5L2026-07-09 17:38
  4. 6e0ff6fneeds attentionfull1H · 5M · 5L2026-07-09 17:17current