fix/cfdi-columns
needs attentionviewing older commitc4c442f · fullpre-PRreviewed 2026-08-04 17:57 UTC0H · 8M · 9L · 6I- 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
- 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+709−2docs/development+182−0domains/utility/data/current-catalogs+164−2domains/utility/src/bill-concepts-catalog+120−4packages/database+78−0scripts+448−0services/utility/bills/cfe+327−1
- 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.
Findings · 23
correctness4
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.
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.
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.
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
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.
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
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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
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.
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'.
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.
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.
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.
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.