fix/next-check-ui
needs attentionviewing older commitc531302 · fullPR #313reviewed 2026-07-15 23:47 UTC0H · 2M · 5L · 3I- 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
- 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+236−0apps/platform/src/app/[locale]/(dashboard)/bills/contratos+30−4domains/utility/src+8−0infra/cdk/src/stacks/services/utility/bills/cfe+4−0
- Blast
- 7 files, +278/−4; pure display-layer change — no DB migrations, no API changes, no schema changes. Backend files: comment-only additions.
Findings · 11
correctness3
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.
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.
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
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.
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
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.
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.
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
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.
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.
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.