← all branches

fix/next-check-ui

needs attentionviewing older commit
c531302 · fullPR #313reviewed 2026-07-15 23:47 UTC0H · 2M · 5L · 3I
The branch
Purpose
Fix the 'Próxima' column in the Contratos pipeline cell so it shows the real next cron fire time, never a past date
Goal
Compute and display when the daily collect/payment cron will actually query CFE for each RPU — replacing the raw nextExpectedBillDate (which stays in the past once a bill is overdue) with a forward-looking prediction that mirrors the backend's due-gate logic
Sub-goals
  • SG-1: Extract pure date-math helpers (next-check.ts) that mirror the backend's COLLECT_GRACE_DAYS/PS_GRACE_DAYS gates and EventBridge cron hours
  • SG-2: Wire helpers into PipelineCell to replace the stale nextExpectedBillDate/paymentNextCheckAt reads for the 'Próxima' display
  • SG-3: Rename field-registry label to match what the DB column actually carries (expected-bill date, not next-cron-fire)
  • SG-4: Add TWIN cross-reference comments to backend files so future grace/cron-hour changes are not missed
The changes (whole branch)
What
New next-check.ts utility (108 lines) with 3 pure exports; 128-line test suite; PipelineCell wired to use the helpers and cadence derived from billing cycle; field-registry label corrected; TWIN comments added to 3 backend files
Why
The previous code showed nextExpectedBillDate as 'Próxima consulta' which could be days in the past for overdue subscriptions — misleading operators about when the cron would actually fire next
Areas
apps/platform/src/app/[locale]/(dashboard)/bills/_lib+2360apps/platform/src/app/[locale]/(dashboard)/bills/contratos+304domains/utility/src+80infra/cdk/src/stacks/services/utility/bills/cfe+40
Blast
7 files, +278/−4; pure display-layer change — no DB migrations, no API changes, no schema changes. Backend files: comment-only additions.
display-only no-db-change pure-functions well-tested
typecheck· not run in this triggertests· no CI checks on this PRcoderabbit· no .coderabbit.yaml

Findings · 11

correctness3

medium

Payment suite missing exact-boundary test for the due-tick rollover

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/__tests__/next-check.test.ts:86

The collect suite has an explicit test for 'exactly at the due tick' (now === dueTick → strict-greater check fails → rolls to next day). The payment suite has no equivalent. Both share the same logic (`dueTick.getTime() > now.getTime()`), so a future refactor that changes `>` to `>=` in one but not the other would go undetected. Add a symmetric boundary test for the 'paid' branch.

low

45-day Bimestral threshold is magic with no documented basis

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:107

cycleDays >= 45 is correct given only 31 (monthly) and 62 (bimonthly) are valid CFE cycles, but there's no comment tying the constant to those values. A brief note (e.g. '// CFE cycles: 31 monthly or 62 bimonthly — 45 is the midpoint') would make the invariant auditable.

low

field-registry paymentNextCheckAt label may confuse users comparing it with the strip

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/_lib/field-registry.ts:326

The registry entry for paymentNextCheckAt (label: 'Próxima consulta de pago') reflects the DB-persisted server value. PipelineCell now computes and displays a different render-time prediction via computeNextPaymentCheckAt. These can differ: the DB value is written by the Lambda after each run; the computed value is the frontend's guess for the next cron fire. A maintainer note mirroring the nextExpectedBillDate comment style (lines 248-251) would make the distinction explicit.

conventions2

medium

Single GRACE_DAYS couples two independently-tracked backend constants

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:26

The backend declares COLLECT_GRACE_DAYS and PS_GRACE_DAYS as separate named constants (monitoring-subscription.queries.ts and payment-monitoring-subscription.queries.ts), even though both are 3 today. The UI collapses them into a single GRACE_DAYS. If one pipeline's grace period changes independently, the UI will silently mispredict the other. Rename to COLLECT_GRACE_DAYS and PAYMENT_GRACE_DAYS to match the backend's structural intent.

low

Cadence labels hardcoded as raw Spanish string literals (pre-existing pattern extended)

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:103

deriveCollectCadenceLabel returns 'Mensual' | 'Bimestral' as raw strings. ui-patterns.md mandates next-intl translation keys for user-facing strings. PipelineCell has no useTranslations throughout (pre-existing), so this PR extends rather than introduces the pattern. Low risk now; worth addressing in a dedicated i18n pass.

tests3

low

Malformed nextExpectedBillDate fallback path not tested

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:39

tickOnDueDate returns null on non-date strings (e.g. 'not-a-date'). Both compute functions silently fall back to the daily tick. This defensive path is correct but untested. A simple test with 'not-a-date' would lock in the graceful degradation.

low

computeNextPaymentCheckAt missing 'never returns a time in the past' invariant test

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/__tests__/next-check.test.ts:69

computeNextCollectCheckAt has an explicit loop asserting the result is always in the future (line 35-42). The payment function has no equivalent. Both share nextDailyTick, so risk is low, but symmetry would catch a regression if the payment path ever diverges.

info

45-day Bimestral threshold boundary not tested

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/__tests__/next-check.test.ts:108

The threshold at exactly 45 days is not exercised. CFE cycles are always 31 or 62 days in production, making this a theoretical gap only. Worth adding for completeness if the suite ever covers adversarial data.

improvement3

low

tickOnDueDate regex captures three groups but only uses match[0]

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:39

The regex /^(\d{4})-(\d{2})-(\d{2})/ captures year/month/day in groups 1-3, but only match[0] (the full match) is consumed. Could use /^\d{4}-\d{2}-\d{2}/ (no capture groups) or just dateOnly.slice(0, 10) to make the intent clearer.

info

now = new Date() captured once per render — staleness bounded but acknowledged

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/_components/PipelineCell.tsx:177

The existing comment correctly documents the staleness trade-off. For reference: a useMemo with a ~60s setInterval invalidation or a useNow(60_000) hook would keep relative countdowns fresh without per-ms re-renders, if user feedback surfaces this as an issue.

info

deriveCollectCadenceLabel silently defaults Mensual for reversed or invalid dates

apps/platform/src/app/[locale]/(dashboard)/bills/_lib/next-check.ts:100

If lastBillDate > nextExpectedBillDate (data error), daysBetween returns a negative number, which is < 45, so returns 'Mensual'. A cycleDays > 0 guard before the threshold check would make the silent fallback explicit. The comment on line 105 partially documents this but doesn't cover the reversal case.

History · 3 commits

  1. b2c23d7safeincremental0H · 0M · 0L2026-07-16 00:14
  2. c531302needs attentionfull0H · 2M · 5L2026-07-15 23:47current
  3. a52a19dneeds attentionfull2H · 5M · 3L2026-07-15 23:33