← all branches

fix/cfdi-columns

needs attentionviewing older commit
c4c442f · fullpre-PRreviewed 2026-08-04 17:57 UTC0H · 8M · 9L · 6I
The branch
Purpose
Fix CFDI fiscal data quality: text columns rendered as numbers, missing stamp status and fiscal error concepts in historical bills
Goal
Surface CFDI stamp state and error details in the bills UI; backfill 143k historical XML-backed bills; add Energy Strategist job posting
Sub-goals
  • Fix decideColumnType to derive render type from catalog units instead of hardcoding 'number'
  • Add 4 CFDI fiscal concepts to all 11 tariff catalogs
  • Update XML parser to extract cfdi:Complemento/TimbreFiscalDigital fields and derive stamp status
  • Add parseOrgs/orgInFilter ops-guard helpers for per-org backfill scoping
  • Add idempotent backfill script with --expect-ref safety guard and 6-step runbook
  • Add rollback script that strips the 4 backfilled concepts
  • Publish Energy Strategist job posting to marketing site
The changes (whole branch)
What
Two independent workstreams: (1) CFDI fiscal — fixes text-as-number rendering class bug, adds 4 new fiscal concepts to all tariff catalogs, updates XML parser to walk Complemento, provides idempotent backfill/rollback scripts; (2) Marketing — Energy Strategist job posting with JobPosting schema, careers index and sitemap updates
Why
Customers (Grupo Axo, Pilgrim's Pride) reported CFDI stamp status missing from bills table; root cause was parser never walking cfdi:Complemento and the column type fallback pushing all text concepts through parseFloat
Areas
apps/web+7092docs/development+1820domains/utility/data/current-catalogs+1642domains/utility/src/bill-concepts-catalog+1204packages/database+780scripts+4480services/utility/bills/cfe+3271
Blast
27 files, +2040/-9 across 4 domains. decideColumnType change has widest UI blast radius — affects all ~460 line-item concepts in bills table. Backfill is one-off ops run, not part of production deploy.
contains-backfill-script ops-guards-extended web-job-posting
typecheck· CI not available — gh auth not configured on runnertests· CI not available — gh auth not configured on runnercoderabbit· no .coderabbit.yaml in repo

Findings · 23

correctness4

low

FechaDeTimbrado/FechaTimbrado attribute name mismatch undocumented

services/utility/bills/cfe/src/file-processor/xml-parser/xml-parser.ts:438

fillWhenBlank fills 'FechaDeTimbrado' from timbre['@_FechaTimbrado'] — CFE Addenda vs SAT spec naming divergence. The mapping is correct but a one-line comment explaining why the attribute name differs from the stored key would prevent future confusion.

info

CANONICAL.json totalConcepts 459→463 is consistent with actual file content

domains/utility/data/current-catalogs/CANONICAL.json:3

+4 total: errorDescription + errorLocation added in earlier commits, errorCode + cfdiStampStatus added here. grep -c conceptName CANONICAL.json returns 463. Not a bug.

info

Idempotency guard treats cfdiStampStatus-present as fully processed — documented design

scripts/backfill-cfdi-fiscal-concepts.ts:191

Guard also fires for live-parser bills (post-deploy). Correct for first-run flow. Operators re-running after a catalog re-seed should know the guard won't refresh conceptIds for already-processed bills.

info

orgInFilter OR-clause binds same list twice — intentional, no false-positive risk

packages/database/src/ops-guards.ts:133

org_ prefixed strings cannot match uuid column and vice versa. The double-binding is the correct implementation for mixed public_id/uuid input.

conventions2

medium

Multi-line JSDoc on decideTypeFromUnits/decideColumnType violates comment convention

domains/utility/src/bill-concepts-catalog/bill-concepts-catalog.decisions.ts:268

CLAUDE.md: no multi-line docstrings. Both helpers carry multi-paragraph JSDoc blocks. Condense to a single-line comment noting the parseFloat bug class.

info

parseOrgs/orgInFilter in packages/database carries domain join knowledge — matches existing pattern

packages/database/src/ops-guards.ts:103

orgInFilter encodes the site_utility_contracts→sites→organizations join in the infra package, same tradeoff as rpuInFilter. Not a new regression.

tests7

medium

decideColumnType: paymentStatus and billingFrequency overrides untested

domains/utility/src/bill-concepts-catalog/__tests__/bill-concepts-catalog.decisions.test.ts:14

COLUMN_TYPES has two non-obvious overrides not exercised: paymentStatus→'payment_status' and billingFrequency→'text'. A typo changing either would silently pass the current suite.

medium

orgInFilter: UUID-format org identifier path untested

packages/database/src/__tests__/ops-guards.test.ts:155

The feature accepts 'organization public_id OR uuid' but only org_ prefixed values appear in tests. A plain UUID (e.g. '550e8400-e29b-41d4-a716-446655440000') is never exercised, leaving the uuid id branch uncovered.

medium

decideTypeFromUnits payment_status branch has no test path

domains/utility/src/bill-concepts-catalog/__tests__/bill-concepts-catalog.decisions.test.ts:73

decideTypeFromUnits has case 'payment_status' (decisions.ts ~line 284) but no test calls decideColumnType with a concept NOT in COLUMN_TYPES whose units='payment_status'. Dead-code risk for future concepts.

low

xml-parser: clsRegArchFact in <Recibo> wrapper path untested

services/utility/bills/cfe/__tests__/unit/file-processor/xml-parser.test.ts

parseCfeXml and extractFolio both handle clsRegArchFact inside <Recibo> but no test exercises this alternative XML shape.

low

xml-parser: formatYearMonth/OCR_AAMM derivation untested

services/utility/bills/cfe/__tests__/unit/file-processor/xml-parser.test.ts

formatYearMonth is in the main parse path but no test passes OCR_AAMM and asserts OCR_AAAAMM output. The trailing-'.0' stripping edge case is also uncovered.

low

Five exported xml-parser utility functions have no direct tests

services/utility/bills/cfe/__tests__/unit/file-processor/xml-parser.test.ts

parsePeriodRange, deriveYearMonth, parseIntSafe, parseNumSafe, parseNumOptional are exercised indirectly only. Edge cases (malformed dates, non-numeric strings, null returns) have no explicit coverage.

info

Backfill/rollback scripts have no unit tests — consistent with existing ops script pattern

scripts/backfill-cfdi-fiscal-concepts.ts:115

guardConnection/parseOrgs/rpuInFilter are all covered in ops-guards.test.ts. parseSince() could be extracted to ops-guards to get coverage cheaply.

improvement4

medium

generate-catalogs.mjs not updated — will overwrite new CFDI concepts on next run

domains/utility/data/current-catalogs/generate-catalogs.mjs

The 4 new CFDI fiscal concepts were hand-appended to 11 tariff JSONs and CANONICAL.json but generate-catalogs.mjs KNOWN_UNITS map has no entry for them. Running the script in the future will silently overwrite the per-tariff JSONs, dropping all 4 concepts. Either update the CSVs and KNOWN_UNITS map, or add a PASSTHROUGH_CONCEPTS list to the generator.

low

parseOrgs duplicates parseRpus body verbatim — extract shared helper

packages/database/src/ops-guards.ts:103

parseOrgs and parseRpus are structurally identical (same loop, dedup/trim/filter) differing only in the flag name. A generic parseMultiValueFlag(flag, argv) would eliminate copy-paste and prevent drift.

low

Two consecutive idempotency guards in backfill — second is effectively unreachable

scripts/backfill-cfdi-fiscal-concepts.ts:191

First guard (line 191) skips when cfdiStampStatus present AND needsRepair empty. Since cfdiStampStatus is always written last, the second guard (needsAppend+needsRepair both 0) is only reachable in a scenario the comments say cannot occur. Merge into one clearly-commented early-exit.

low

Job posting page layout fully duplicated — no shared component yet

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:159

energy-strategist (686 lines) and full-stack-engineer-ai-native share identical structural shell. Pattern is establishing — a JobPostingLayout component would prevent a third posting adding another 600+ line page.

seo6

medium

Meta description 179 chars — 19 over the 160-char limit

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:9

Search engines truncate at ~160 chars, losing the 'Híbrido CDMX' CTA. Remove 'Perfil de ingeniería, ' (22 chars) — 'sin experiencia en ventas requerida' already implies the profile.

medium

Title suffix should be '| Batu', not '| Carreras Batu Energy'

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:8

CLAUDE.md prescribes 'keyword first, | Batu suffix'. 'Energy Strategist | Carreras Batu Energy' is non-standard. Correct: 'Energy Strategist | Batu'.

medium

Missing BreadcrumbList JSON-LD for nested /carreras/energy-strategist route

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:40

CLAUDE.md requires BreadcrumbList schema for nested routes. Add: Inicio → Carreras → Energy Strategist.

low

JobPosting schema missing jobLocationType for hybrid role

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:52

Role is 'Híbrido · CDMX' but schema has no jobLocationType. Add jobLocationType: 'TELECOMMUTE' alongside physical jobLocation to enable Google Jobs hybrid filter eligibility.

low

full-stack-engineer validThrough expires 2026-08-13 — 9 days away

apps/web/src/app/(marketing)/carreras/full-stack-engineer-ai-native/page.tsx:64

Google removes JobPosting rich results once validThrough passes. Update before 2026-08-13 if the position is still open.

info

JobPosting schema could add baseSalary — salary already disclosed in page content

apps/web/src/app/(marketing)/carreras/energy-strategist/page.tsx:40

The page mentions salary in Qué ofrecemos. Adding baseSalary would unlock Google Jobs salary display. Not required for validity.

History · 4 commits

  1. f88ee88needs attentionincremental0H · 1M · 1L2026-08-10 16:25
  2. 0869f39needs attentionfull1H · 8M · 5L2026-08-05 13:58
  3. 74e05c5safeincremental0H · 0M · 0L2026-08-04 22:43
  4. c4c442fneeds attentionfull0H · 8M · 9L2026-08-04 17:57current