Commit Graph
111 Commits
Author SHA1 Message Date
rmancinasandClaude Opus 4.8 83e3cb8f47 Transform+load: shared ledger + SCOTHIA bank register (step 3 complete)
migration/transform_transactions.py unions every cash/billing ledger into
`transactions` per the reconciliation rules: both EFECTIVO tables (no folio
de-dup, near-disjoint), all three billing tables (disjoint periods), the FM3
fee stream (amount = fee+tax+multa), IVA 2015 (nominal date), and insurance
EFECTIVO (domain INSURANCE). Also loads the type_transactions (EN/ES) and
exchange_rates lookups. Customer FK resolves through customer_legacy_refs;
rows with no resolvable customer/date are skipped and counted.
Loaded (dev): 45861 transactions (UTILITY 45566 / INSURANCE 295, 0 orphans),
79 type_transactions, 2301 exchange_rates.

migration/transform_bank.py loads SCOTHIA DATOS I/E into bank_transactions as
signed amounts (income +, expense -) and TABLA RAMODOS into
business_line_categories. Deliberately customer-independent (office's own
checking account). Loaded (dev): 22354 bank_transactions (net +899,375.77),
66 categories; categoryId left null (concept->ramo classifier is future work).

run_all.py: pipeline now customers -> properties -> policies -> transactions
-> bank, all idempotent. Verified full end-to-end run against dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:43:34 -07:00
rmancinasandClaude Opus 4.8 21899e99bb Transform+load: consolidate all insurance lines into policies (step 3)
migration/transform_policies.py folds every insurance Access table into one
`policies` table (policy_types discriminator) plus child tables, via a
per-table declarative mapping that absorbs the column-name variance
(num_id/numer_id, no_poliza/poliza, p_neta/prima_neta/prima1). Any source
column not explicitly modeled — the type-specific coverage amounts — is
preserved verbatim in coveragesJson, so consolidation loses nothing.

Unpivots the hardcoded repeated slots: 4 payment installments (c_1er_pago +
pago_subsec x3), up to 3 vehicles (auto tables + MCA2), up to 3 named
insured drivers (MCA2 + LICENCIAS). Also loads BENEF -> policy_beneficiaries
(by policy number), DATOS -> claims, AJUSTADORES(+ATLAS) -> adjusters, and
builds policy_types + insurance_providers lookups.

Loaded/validated (dev): 2378 policies (AUTO 1307 / MULT 760 / LICENCIAS 306 /
M_EMPR 5; 10 skipped for unresolved customer FK, 0 orphans), 4678
installments, 1110 vehicles, 513 drivers, 126 beneficiaries, 1 claim, 15
providers, 17 adjusters — all child FKs verified 0 orphans. Spot-checked a
customer carrying both a utility property and MULT policies (the unified
cross-line view).

run_all.py: add policies to the ordered pipeline. Customer FK resolves
through insurance customer_legacy_refs, so this runs after customers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:36:29 -07:00
rmancinasandClaude Opus 4.8 a680ad2bb0 Transform+load: properties/services/trust + env-parameterize migration
migration/transform_properties.py loads properties, property_services and
trust_accounts from staged DATMEX/PROFILE, resolving each property's customer
FK through customer_legacy_refs. Services are derived from DATMEX's own
account/route/meter fields (the authoritative data); PROFILE flags — merged
best-effort on (numer_id,casa,direccion), which matched 1519/1519 — only
refine each service's `active`. Trust accounts are 1:1 from DATMEX trust
fields; TRUSTVENCE (overlapping) deferred to reconciliation; blobs are step 4.

Loaded/validated (dev): 1519 properties (0 orphans, 1 blank id skipped),
3486 services (ELECTRIC 1118 / PROPERTY_TAX 939 / WATER 859 / GAS 335 /
OTHER 115 / FEDERAL_ZONE 76 / CABLE 41 / ALARM 3), 553 trust accounts —
counts track the PROFILE enrollment flags.

Reproducibility (asked: dev must be redoable in prod):
- migration/dbenv.py: single DB-target source = deploy/.env.<env>'s
  DATABASE_URL; connect(env) + env_arg() (--env, default dev).
- transform_customers.py / transform_properties.py now take --env instead of
  hardcoding .env.dev.
- migration/run_all.py: runs every step in dependency order against --env
  (optional --stage re-extracts from Access first). Reproducing dev->prod is
  `run_all.py --env prod` after deploying the prod stack + prisma db push.

All steps are idempotent (truncate+rebuild); re-run yields identical counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:29:31 -07:00
rmancinasandClaude Opus 4.8 ec499ca5f5 Transform+load: unified customer master (migration step 3, customers)
migration/transform_customers.py builds `customers` + `customer_legacy_refs`
from staged DATGRAL, implementing the reconciliation rules: utilities DATGRAL
is the customer master; insurance DATGRAL folds in via its num_util
cross-reference (matches enrich the master with the ID-document fields
utilities lacks); COBRO3 excluded as a charge batch. Every legacy row gets a
provenance ref, so the load is auditable and idempotent (truncate+rebuild).

Loaded and validated against the dev DB (192.168.4.212:3307):
  1682 customers (1172 utilities master + 510 insurance-only)
  2242 legacy refs (1172 utilities + 1070 insurance) — 0 orphans
  560 insurance rows linked via num_util, 0 broken cross-refs
  542 merged identities spanning both business lines
Spot-checked a merged customer: single record carrying utilities fee +
insurance passport/ID enriched in, both provenance refs present.

RESUME.md: mark customers done, record dev-DB infra + the pnpm/npm caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:18:10 -07:00
rmancinasandClaude Opus 4.8 b5983ba687 Add Swarm MySQL stack for cubex (dev/prod), deploy dev
deploy/jorgecuadros-db.stack.yml: canonical internal MySQL for the platform,
targeting the Portainer local endpoint on cubex (3-node Swarm). Parametrized
(MYSQL_PORT / MYSQL_SERVER_ID) so one file deploys both environments as two
stacks with Swarm-namespaced volumes:
  dev  -> jorgecuadros-dev-db  :3307  server-id 11
  prod -> jorgecuadros-prod-db :3306  server-id 1  (replication source)

Swarm-correct: named volume (no bind mount), pinned to one node via
node.labels.jorgecuadros_db==true (cubex labeled), binlog+GTID enabled from
the start so prod can be the VPS replication source without reconfigure.

Dev deployed and verified: MySQL 8.4.10 reachable at 192.168.4.212:3307,
gtid_mode ON, database jorgecuadros present. Secrets live in gitignored
deploy/.env.dev, injected via Portainer stack env at deploy time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 18:13:38 -07:00
rmancinasandClaude Opus 4.8 81f62430b3 Reconciliation pass (migration step 2): decide union/de-dup rules
Add migration/reconcile.py (reads staged Parquet) and the generated
migration/RECONCILIATION.md. Naive full-row matching across the suspected
"duplicate" groups gives a misleading ~0 overlap, so each group is probed on
a deliberate business key instead. The evidence overturns all three of the
plan's original assumptions:

- EFECTIVO vs EFECTIVO_BACKUP: NOT a live/backup pair. `folio` is a per-table
  sequential number that collides (12,363 shared folio numbers, every one a
  different transaction); real business-key (cl,fecha,monto,conepto) overlap
  is 2. Near-disjoint ledgers (BACKUP ~2017-2022, EFECTIVO recent). Rule:
  migrate both, keyed by (source_table, folio) provenance, no folio de-dup,
  don't drop BACKUP. FM3 tables are a separate fee/tax/multa stream.
- datos2 vs FEE ANUAL vs fee15: disjoint billing runs from different periods
  (2025-26 / 2018 / 2017), zero real-identity overlap. Rule: union all three,
  no de-dup; keep datos2.due_date.
- DATGRAL vs COBRO3: COBRO3.fee is a constant 75 (a charge batch), not a
  filtered customer snapshot; every num_id already in DATGRAL. Rule: DATGRAL
  is the sole customer master, COBRO3 contributes zero customers.

Also flags monedas currency variants (PESOS/Pesos/DOLLARS) for normalization
at transform time.

Update PLAN.md (migration step 2 outcome + corrected inventory bullets) and
RESUME.md (queue: reconciliation done, transform+load next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:23:33 -07:00
rmancinasandClaude Opus 4.8 dc9866c24a Port extraction layer from pyodbc to mdbtools for macOS
macOS has no Access ODBC driver, so the pyodbc-based extract.py could not
run. Rewrite it to shell out to mdbtools (mdb-tables/mdb-export) while
keeping the public interface (connect/list_tables/read_table/
sanitize_column_name) unchanged, so load_staging.py and config.py are
untouched. connect() now returns the file path as the handle (mdbtools has
no persistent connection).

Behavior details:
- -b strip drops LONGBINARY/OLE bytes so blobs never corrupt the CSV
  (documents are extracted separately in migration step 4).
- ISO date/datetime output (-D/-T); staging read as text (dtype=str),
  only empty fields treated as null (keep_default_na=False) so literal
  "NA"/"NULL" data strings survive.
- mdbtools reads deleted/corrupted records mdbtools omits rather than
  aborting, so the old per-row skip loop is no longer needed.

Verified end-to-end: full staging load reproduces the original Windows
run (82 tables, 0 errors) with exact row counts (datos2 16000, EFECTIVO
13697, DATGRAL 1172/1070, DATMEX 1520) and recovers all 764 MULT rows
(the pyodbc path lost 1 to HY109 corruption). Accented-column tables
(PROPANO) read cleanly.

requirements.txt: drop pyodbc/pywin32 (Windows-only), keep pandas/pyarrow/
sqlalchemy/pymysql; document the Windows-only DAO catalog as historical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:15:02 -07:00
rmancinasandClaude Opus 4.8 22a53bc562 Lock plan: macOS move, mdbtools extraction, resolve open decisions
Project moved Windows -> macOS, retiring the pyodbc + Access ODBC driver
extraction path in favor of mdbtools (verified against the real files).
Record the four now-locked decisions and the resolved utility_dbo blocker.

- PLAN.md: rework migration step 1 for mdbtools; replace "Open decisions"
  with locked Decisions (extraction=mdbtools, i18n=Spanish-first,
  CI=Gitea Actions, utility_dbo=available); update Status for the macOS
  move + portal DB in hand; finalize sync steps against real utility_dbo.
- RESUME.md: rewrite next-steps as a locked execution queue.
- migration/config.py: point SOURCE_ROOT at ~/Downloads/JorgeCuadros-Legacy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:08:43 -07:00
rmancinas 0268ed896c Add Queries/Forms/Reports reference (DAO catalog)
ODBC only exposes Tables and non-hidden SELECT queries, so this used
DAO COM automation (migration/catalog_objects.py, requires pywin32)
to catalog Reports, Forms, and full Query SQL text across all four
files instead.

Key finding: SEGUROS 16.mdb, previously noted as having zero data
tables, turns out to hold all 212 Reports/149 Forms/857 Queries for
the insurance line - SEGUROS 16_be.mdb is confirmed pure data storage
with zero saved objects. The renewal-notice reports also reveal a
RENEW/RENEW2/RENEW3 multi-notice reminder cycle not visible in the
table schema.

docs/LEGACY_DATABASES_OBJECTS.md documents all of this with the full
751 real queries' SQL text (business logic: billing math, year-
rollover batches, duplicate/delinquency detection). Raw output at
migration/objects.json. Cross-linked from RESUME.md and the existing
table-only LEGACY_DATABASES.md.
2026-07-22 01:50:22 -07:00
rmancinas 7e65fc81d5 Add legacy database structural reference for non-Windows machines
docs/LEGACY_DATABASES.md documents all three source Access databases
(every table, column, type, and known data-quality quirk) generated
from a live read of the real files, so no Windows/Access driver is
needed to understand their structure going forward.

New migration/ tooling: catalog_schema.py connects to the real files
and walks every table (including excluded scratch tables);
render_catalog_md.py renders that into the doc's appendix. Raw output
checked in at migration/catalog.json so the doc can be regenerated
without touching Access again.
2026-07-22 01:41:53 -07:00
rmancinas 27118f0df2 Initial scaffold: unified customer/insurance/utilities platform
Next.js + NestJS + Prisma (MySQL) monorepo replacing the legacy PHP
internal app. Includes a session-based auth module with Argon2 password
hashing and global input validation (replacing the old app's SQL
injection and plaintext password comparison), the full target Prisma
schema for customers/insurance/utilities/shared ledger/bank register,
Docker Compose + Dockerfiles, and an Access-to-staging migration
pipeline (migration/) already run against the real source databases.

See PLAN.md and RESUME.md for the full architecture and session history.
2026-07-22 01:29:56 -07:00