4ee7ec71f00e068a37bb25657bc44760ac0bc02c
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4ee7ec71f0 |
feat(deploy): prisma migration history, /version, galactus standalone deploy
Closes the gap between "what tag did I deploy" and "what is actually running", and gives the schema a history that can be reasoned about across releases. Migrations - Baseline the existing schema as 0000_init (migrate diff --from-empty). The schema had only ever been applied with `prisma db push`, so no history existed and schema state was disconnected from app version. Existing databases must be baselined once with `migrate resolve --applied 0000_init`; the workflows print this remedy on P3005. - Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a CMD, N replicas would race each other applying the same migration. Version reporting - GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that build.yml already baked into both images but nothing ever read. - The web footer shows the web build and flags an api/web mismatch. The two cannot drift at build time (one matrix run) but can at deploy time. - Both deploy workflows now fail if the running API does not report the tag that was dispatched — a stack naming a tag is not proof of what is running. - scripts/set-version.mjs stamps every package.json, which had all sat at 0.1.0 while real releases shipped as v1.x. Pre-migrate backup - deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the still-running old API container over Portainer's Docker API, so the file lands in the volume the Operaciones restore screen reads. A dump taken on the CI runner would be unreachable by the only restore path we have. Verifies the artefact with `gzip -t` before letting the migration proceed. galactus - deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks. Plain compose silently ignores `deploy:`, so restart_policy becomes `restart: unless-stopped` — without it nothing returns after a host reboot. - .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets. Fixes - deploy.yml passed `endpoint_id` and `pull_image` to cssnr/portainer-stack-deploy-action, which has no such inputs (they are `endpoint` and `pull`). The endpoint was silently never set. docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema changes: Prisma has no down-migrations, so a code rollback never rolls the schema back, and restoring the replication master from a dump diverges every replica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9ba5d2d09a |
feat(bank): multi-bank chequera — required bankAccountId, per-account scoping
The office keeps more than one operating account (Utilities banks in MXN, Seguros in USD), but bank_transactions was a single implicit MXN register by design. Adds Bank/BankAccount and makes every read and write in the module scoped to exactly one account. Schema: - Bank / BankAccount. Currency is fixed per account and BankTransaction has no currency column of its own — a movement inherits its account's, the way a real bank account doesn't mix currencies. - BankTransaction.bankAccountId, required. A movement with no known account isn't reconcilable against a statement. - @@index([bankAccountId, transactionDate]): every read now filters by account and orders/groups by date. Migration: - backfill_bank_accounts.py seeds Scotiabank + "Utilities — Scotiabank (MXN)" and backfills all 22,669 existing rows onto it, then promotes the column to NOT NULL and attaches the FK. Standalone because prisma db push cannot add a required column to a populated table. Idempotent; re-running once a second account exists does not re-point rows. - run_all.py runs it (both modes) before transform_bank.py, which now resolves the account by label and fails fast if it is missing. API: - ?bankAccountId= required on list/stats/facets/summary — not optional with an "all accounts" default, since summing an MXN and a USD register repeats the currency-collapsing mistake the billing module exists to prevent. Missing is 400, unknown is 404. - facets() had no account clause at all and summary() has two raw-SQL rollups; all three are now parameterised. Scoping only one of summary's queries would leave the year list and its drill-down describing different books. - New bank/accounts + bank/banks sub-resource under a MANAGER bank:manage-accounts ability. currency is absent from the update DTO: booked movements are denominated in it, so editing would re-denominate history. Capture into a closed account is rejected. Web: - /banco gains an account picker (remembered per browser) and reads every figure in the selected account's currency; the "single currency (MXN)" doc-comment and the hardcoded MXN formatting are gone. - New /banco/cuentas for banks and accounts. Accounts are closed, never deleted — the FK is required, so deleting one would destroy its register. - /inicio's chequera card names the account it is reading instead of implying a single register. Verified against dev + browser: a second USD account showed full read/write isolation from the MXN register, whose totals were unchanged (22,669 movements, net 1,014,266.97). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c100dfa224 |
feat(web,api): scale spacing with text size, persist preference per account
Two follow-ups to the text-size control. Spacing now scales with the text. All padding, margin, gap and min-height declarations in globals.css move from px to rem (263 declarations, converted mechanically), so --ui-scale drives the whole layout rather than just the glyphs. Deliberately left in px: border widths, which must stay hairlines; box-shadow offsets; border-radius, which reads as bloated when scaled on large cards; --shell-max, a container cap that must not outgrow the viewport; and media-query breakpoints, which are conditions rather than declarations. With spacing following along, the presets gain a 1.5 "Máximo" step and MAX_UI_SCALE rises from 1.4. The preference now lives on the account instead of only in one browser. User.uiScale (Float, default 1) is added to the schema and to the safe select, so it rides along on /auth/login and /auth/me. PATCH /auth/preferences writes it, guarded by AuthenticatedGuard only — every role including VIEWER may set their own, and the target is always the session's user id, never a body parameter, so this cannot be used to touch another account. The global ValidationPipe's whitelist rejects any extra field, so role cannot ride in alongside uiScale. localStorage stays, demoted to a pre-paint cache for the layout.tsx script; AppShell reconciles it against the account once /auth/me answers, with the account winning. FontScaleControl becomes a controlled component since the same value is now edited from the appbar and the drawer. Verified against the dev API: PATCH persists and is reflected by a subsequent /auth/me, out-of-range values are rejected 400, and an extra "role" field in the body is rejected 400. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7df928c3ab |
feat(billing): receipt capture — outstanding workflow, batch by check, reconciliation
Implements docs/RECEIPT_CAPTURE_SPEC.md §1, the legacy "Editor" replacement, on top of the single-movement capture from plan step 6. No new abilities: batching and resolving are both capturing. - outstanding (legacy NOPAGO): capture flag, ?outstanding= filter, and POST /billing/:id/resolve-outstanding (gated ledger:create, not ledger:void — resolving completes a capture rather than reversing one). Outstanding rows are excluded from every balance aggregate, matching the legacy SALDOS ULTIMO 0 query's HAVING NOPAGO = 0, but still count in the movement browser's filtered totals. - POST /billing/batch: many customers' receipts against one check, in one $transaction. Deliberately not a persisted batch entity — checkNumber is already a column and grouping by it answers every legacy by-check query. - GET /billing/by-check + a cheque-count report, replacing REPORTE CHEQUE COUNT / REPORTE POR CHEQUE / EDITA CHEQUE ALF|COUNT|NUM. Print, PDF, CSV and XLSX come free from the existing /reportes/:slug machinery. - Web: /estado-cuenta/lote (the Editor screen, with live reconciliation against the physical check amount), an "Estado de pago" filter, a "sin fondos" row tag and a Resolver dialog, plus a top-level "Captura" nav entry. Integration seam for the OCR auto-capture module (spec §2), which is required to post through createBatch rather than writing Transaction rows itself: items[i] maps to lines[i] so postedTransactionId can be zipped back on; opts.refs[i] stamps captureRef with a duplicate-post guard that a voided row deliberately does not block; opts.source is service-level only, so an HTTP client cannot label hand-keyed rows as machine-captured. captureSource/captureRef are nullable so the 40,136 migrated rows stay NULL rather than being mislabelled. Fixes two pre-existing bugs found while building this: - statement() filtered legacySourceTable with `notIn`, which compiles to SQL NOT IN — and `NULL NOT IN (...)` is NULL, so every app-captured movement was invisible on the customer statement (438 rows in the movement browser vs 392 on the statement) while showing everywhere else. This would have made the whole capture feature look broken. - The balances count query omitted the void filter its own page query applied, so the total disagreed with the rows. Nav highlighting now resolves by longest match; the previous first-startsWith logic lit up both the parent and any nested entry. Verified end-to-end against the dev DB, API and browser; all test rows removed afterwards. Also corrects RESUME.md, which documented the dev ports as :3001/:3000 — they are :4501/:4500, from the env files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f7ae0d5342 |
feat(reports): parameterized renewal-notice report + legacy report reference
Replaces ~40 legacy Access renewal-notice report clones (one per carrier per coverage tier, e.g. AMPL/RC/LIC RENEW X MES/VENCE ATLAS 13/2013) with one parameterized aviso-renovacion report driven by real Policy/Vehicle/ coveragesJson data instead of hand-typed label text per clone. - schema.prisma: add RenewalNotice, replacing the legacy CONTROL <ramo> RENEW[2/3] X MES paper log of which notice generation was sent - reports: new "letter" ReportFormat + aviso-renovacion registry entry + LetterLayout renderer in ReportRunner.tsx - docs/RENEWAL_NOTICES.md + migration/legacy_report_defs/: extracted (via Application.SaveAsText, since the VBA project wouldn't load) and documented the legacy report/query chain this replaces Coveragesjson key names and a mark-as-sent mutation are still unverified/ unbuilt — see caveats in docs/RENEWAL_NOTICES.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8802f08d4f |
feat(reports): reports module + /inicio + edo-cuenta-datos prefill
- New reports backend (registry, service, controller, outputs, types) with catalog endpoint + slug/CSV/XLSX/PDF/print outputs. - /reportes catalog + /reportes/[slug] runner; ReportRunner + ContextReports components wire pre-filtered links from domain pages. - Fix: /reportes/[slug] now reads searchParams and forwards initialParams to ReportRunner so /reportes/edo-cuenta-datos?customerId=... auto-runs instead of dropping the id and forcing a manual customer search. - /inicio landing page; root + login redirect to /inicio. - Company header env vars + logo asset for PDF/print rendering. - exceljs + pdfkit deps. |
||
|
|
27b3bd9efc | feat: expand admin and data sync workflows | ||
|
|
afe2411c86 |
feat(storage): wire MinIO/S3 document upload & download into the API + web
The schema has carried `storageKey` pointers and the migration has written blobs to MinIO since day one, but the API had no S3 client — documents could only be deleted, never uploaded or retrieved. This adds the missing wiring. API - StorageModule/StorageService (@aws-sdk/client-s3, path-style for MinIO): put/getStream/delete, best-effort bucket ensure on boot, gracefully disabled when S3 env is absent (ServiceUnavailable on use). - Reads S3_ENDPOINT/S3_BUCKET + S3_ACCESS_KEY/S3_SECRET_KEY, falling back to MINIO_ROOT_USER/MINIO_ROOT_PASSWORD so one credential set drives both the migration and the API. - Property service documents: POST :id/documents (multipart), GET :id/documents/:childId/download (streamed), delete now also drops the blob. - Policy documents: same upload/download/delete (previously had none). - Keys stay under the service/<id>/… and policy/<id>/… prefixes the migration established. Web - api.ts: shared uploadFile() helper (uploadIngest refactored onto it), upload/download/remove helpers for property & policy documents. - Servicios, polizas, clientes detail pages: real Descargar links and an upload control (gated by policy:update / property:update) replacing the "storage pending" notes. Infra - docker-compose: minio service (9000/9001, healthcheck, named volume) + S3 env wired into the api service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f1ef1c70b3 |
wip: ops admin panel + migration sync + crud/rbac phase-5 snapshot
Working-tree checkpoint of in-progress work carried across prior sessions on the feat/crud-rbac branch, committed so it lands on the remote alongside the CI changes. - Operaciones admin panel: apps/api/src/ops (ingest upload, backup / restore / re-import jobs) wired into app.module + RBAC abilities, and the apps/web/src/app/operaciones page. docker-compose gets INGEST_DIR / BACKUP_DIR volumes; .gitignore excludes migration/ingest + backups. - migration/sync.py plus transform_*.py / run_all / config / dbenv / blob_extract adjustments for the additive sync path. - crud/rbac phase-5 web bits: AppShell, api/labels/types libs, globals. - schema.prisma + PLAN/RESUME doc updates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7d9f59e51b |
feat(billing,bank): capture + void web UI (plan phase 5 web)
Completes phase 5 — the ledger and chequera pages get the append+void UI on top of the phase-5 API. Web: - Shared MovementForm (customer picker + línea + cargo/abono sign + amount + moneda + concepto facet + periodo/referencia/cheque/mensaje). Used by both /estado-cuenta (cross-customer, picker) and /estado-cuenta/[id] (customer prefilled). - /estado-cuenta and /estado-cuenta/[id]: "Capturar movimiento" toggle gated ledger:create; per-row "Anular" gated ledger:void; voided rows struck-through. Save/void refresh the list + stats. - /banco: inline BankCaptureForm (ingreso/egreso sign, cheque, operado, transferencia, monto en letras) gated bank:create; per-row "Anular" gated bank:void; voided rows struck-through. - api.ts: createMovement/voidMovement, createBankMovement/voidBankMovement; CreateMovementInput/CreateBankMovementInput types; `voided` on the movement/statement/bank list items. Also: lookups.controller.ts now audit-logs provider/policy-type/adjuster create/update/delete (parity with the other write controllers). API + web compile clean. This is the last piece of the feat/crud-rbac branch — all five sections plus users are now full CRUD with role gating. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
548eeb5798 |
feat(ledger,bank): append + void write API, voided excluded from totals (plan phase 5 API)
Transactions and the bank register become append-only with a void (reversal) action — never edited or hard-deleted. This is the API half of phase 5; the capture/void web UI is the remaining piece. Schema: - Transaction and BankTransaction gain voidedAt + voidedById. A non-null voidedAt reverses the row. Pushed to dev. Correctness (the high-stakes part): - Every aggregate excludes voided rows: billing movements totals, the raw balances SQL, stats (groupBy + the sides/crossLine raw subqueries + first/last), facets (types/sources/years); the statement's running balance freezes on a voided row and its per-currency/per-domain/per-type summaries skip them; customers.detail and property owner-ledger groupBy; and every bank total (totalsFor, stats counts/bounds, facets + summary raw SQL). List views still return voided rows with a `voided` flag so the UI can strike them through. - Bank's legacy zero-amount "void" cheques are unchanged and distinct from app voids (voidedAt). API: - POST /billing + POST /billing/:id/void (ledger:create / ledger:void); POST /bank + POST /bank/:id/void (bank:create / bank:void). Create needs STAFF+, void needs MANAGER+. Double-void -> 400, unknown id -> 404, bad date -> 400. Mutations audited. DTOs added. Verified against dev end-to-end: a -500 MXN charge moved a customer balance 31082.08 -> 30582.08, and voiding it returned it to 31082.08 to the cent; a +1234.56 bank ingreso moved net 899375.77 -> 900610.33 and voiding returned it to 899375.77. VIEWER create/void both 403, double-void 400. API compiles clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
506f8ce684 |
feat(properties): CRUD + service/trust/document editors (plan phase 4)
Utilities section becomes create/edit/archive-able, with its child data. API: - Property gains archivedAt (soft-delete); list/browser default to archivedAt=null with ?includeArchived opt-in. - PropertiesService: header create/update/archive/restore (customer FK validated); PropertyService add/update/remove scoped to the property; TrustAccount upsert (1:1) + remove; ServiceDocument pointer delete. - Controller write routes: create needs STAFF+ (property:create), archive MANAGER+ (property:delete), every service/trust/document route property:update. Mutations audited. DTOs added. - Document *upload* deliberately deferred: it needs the object-storage client wired into the API (today only the migration writes to MinIO); removing an existing pointer row is supported and the UI says so. Web: - PropertyForm (header) with CustomerPicker; /servicios/nuevo (accepts ?customerId prefill) and /servicios/[id]/editar. - Property detail: gated action bar (Editar/Archivar) + "Administrar propiedad" — services via the shared ChildCollection editor, an inline 1:1 TrustEditor (create/update/clear), and document-row delete. - "Nueva propiedad" buttons on the list and customer detail (prefilled). api.ts + types for all of it. Verified against dev: property create (archivedAt null), service add/update, VIEWER service-add 403, trust upsert (create then update the same row), trust/service remove, cross-property child guard 404, archive drops from the default list and includeArchived surfaces it. Both apps compile clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7a46c30d9b |
feat(policies): full CRUD + child editors + insurance lookups (plan phase 3)
Policy header, all five child collections, and the insurance reference catalogs become create/edit/delete-able on the RBAC foundation. API: - Policy gains archivedAt (soft-delete); list/browser default to archivedAt=null with ?includeArchived opt-in. - PoliciesService: header create/update/archive/restore (customer FK validated for a clean 404); add/update/remove for installments, vehicles, drivers, beneficiaries, claims — each scoped to its policy so one policy's id can't touch another's rows; lookups CRUD for providers, policy types, adjusters. - PoliciesController write routes: header create/update need STAFF+ (policy:create/update), archive/restore need MANAGER+ (policy:delete), every child route needs policy:update. New LookupsController at /lookups (read open; mutate needs lookup:manage / MANAGER+). Mutations audited. - DTOs (policy header, children, lookups); dates coerced; shared coerce.ts. Web: - Generic ChildCollection editor (config-driven add/edit/remove table), reused by both the policy detail child editors and the catalogs screen. - PolicyForm (header) with type/provider selects and a debounced CustomerPicker; /polizas/nuevo (accepts ?customerId prefill) and /polizas/[id]/editar. Policy detail: gated action bar (Editar/Archivar) + "Administrar detalles" child editors for all five collections. - /catalogos admin screen (aseguradoras/tipos/ajustadores), nav-gated on lookup:manage. "Nueva póliza" buttons on the list and on the customer detail (prefilled). api.ts + types for all of the above. Verified against dev: policy create (dates coerced, archivedAt null), installment/vehicle add, VIEWER child-add 403, cross-policy child guard 404, lookups CRUD with VIEWER 403 / MANAGER 201, archive drops from the default list and includeArchived surfaces it. Both apps compile clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
12692a0af8 |
feat(customers): create/edit/archive CRUD with soft-delete (plan phase 2)
First master-data CRUD module on the phase-1 RBAC foundation. API: - Customer gains archivedAt (soft-delete marker, distinct from the legacy `status` business flag); pushed to dev (nullable, non-destructive). - CustomersService: create/update/archive/restore. list() and the browser default to archivedAt=null; ?includeArchived=true opts in. App-created rows set nameMissing=false and leave legacy provenance null. - CustomersController write routes guarded per the matrix: create/update need STAFF+ (customer:create/update), archive/restore need ADMIN (customer:delete). Every mutation audit-logged. - create/update DTOs (class-validator); date strings coerced to Date. Web: - Shared CustomerForm (create + edit) with identity/address/account sections; new routes /clientes/nuevo and /clientes/[id]/editar, each self-gated on the ability. - List page: ability-gated "Nuevo cliente" button. Detail page: gated Editar / Archivar (Restaurar) action bar; archived badge. - api.ts create/update/archive/restore; CustomerInput type; archived flag on list items. Verified against dev: create (dates coerced, archivedAt null), edit 200, VIEWER create 403, STAFF create 201 but archive 403, ADMIN archive drops the row from the default list and includeArchived surfaces it, restore returns it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
74e2ad8bcd |
feat(auth): role-based permissions + user management (plan phase 1)
Adds the RBAC foundation the CRUD phases build on, and the first write module (users). The platform was read-only: every controller was guarded only by AuthenticatedGuard and UserRole was ADMIN|STAFF. The old PHP app stored level+role but enforced neither, so this is a fresh design. Permission model (server-authoritative): - UserRole expanded to an ordered rank ADMIN > MANAGER > STAFF > VIEWER. VIEWER is the read-only role; STAFF+ can write. - auth/abilities.ts: ROLE_RANK + ABILITY_MIN matrix + can()/abilitiesFor(). - @RequireAbility decorator + AbilityGuard enforce it on write routes; reads stay on AuthenticatedGuard so any logged-in user can read. - /auth/login and /auth/me now return the resolved abilities map, so the web gates its UI off one payload instead of duplicating the rules. User management (ADMIN-only, ability "user:manage"): - UsersService gains list/create/update/resetPassword (argon2), never returns passwordHash; blocks self-deactivation and self-demotion; maps duplicate email to 409. - UsersController: GET/POST /users, PATCH /users/:id, POST /users/:id/reset-password. - Every mutation logged via new AuditService over the existing ActivityLog model (global CommonModule). Web: - AuthContext + useAuth/useCan; AppShell provides the user and gates the new "Usuarios" nav entry on user:manage; shows the user's role. - /usuarios admin page: list + create/edit form + password reset + active toggle, Spanish-first, reusing existing card/table/field styles. Schema pushed to dev (enum only, non-destructive). Verified end-to-end against dev: admin CRUD works, VIEWER writes 403 while reads 200, self-lockout guards and duplicate-email 409 all hold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
db862df8fe |
feat(bank): chequera register module (plan step 7)
Adds the office's own bank-register browser over the migrated SCOTHIA
data (22,354 bank_transactions), the last self-contained feature module.
API (apps/api/src/bank):
- GET /bank register browser: search over concepto/reference/notes/
amountInWords; direction (income|expense|void), cleared and
date-range filters; 5 sorts; income/expense/net totals for
the whole filtered set, not just the page
- GET /bank/stats headline income/expense/net + counts, date span, pending
- GET /bank/facets year list for the period filter
- GET /bank/summary year and month rollups with a running net-movement figure
Web (/banco): "Movimientos" register + "Resumen por periodo" with year->month
drill-down; added to the AppShell nav as "Chequera".
Deliberately kept OUT of /estado-cuenta: this is the office's own money, not
customer balances, and the two are never summed or shown together.
No category/ramo dimension, and the deferred concept->ramo classifier is
dropped as won't-build: concepto is a payee name (0 of 22,354 match a
category) and TABLA RAMODOS is an expense chart of accounts + owner names,
not the insurance/servicios/fideicomiso split it was assumed to be, so a
classifier would invent data. Single currency (MXN); the "acumulado" is net
movement since the register opened (no opening balance in the source), not a
bank balance. Verified end-to-end in the browser; totals reconcile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2c6a6bf60b |
feat(billing): shared statements module across both business lines
Plan step 6 — the payoff of the unified customer record: a utility charge and an insurance payment finally sit on the same page, under the same person, with a running balance. API (apps/api/src/billing/): - GET /billing — cross-customer movement browser. Search over customer, referencia, cheque, concepto and periodo; filters for business line, currency, charge-vs-credit, concept, origin table and a from/to date range; 5 sorts. Returns totals for the whole filtered set, not just the page, so a filtered view can't be misread as the full ledger. - GET /billing/balances — per-customer receivables worklist with owing/credit/settled buckets and 4 sorts. Raw SQL (parameterized via Prisma.sql): needs conditional sums per currency and per direction in one pass plus ordering and pagination on a computed balance, none of which groupBy expresses. - GET /billing/stats, /billing/facets, /billing/customers/:id. Web: - /estado-cuenta — two views over the same ledger, because staff ask two different questions: "Saldos por cliente" (who owes what) and "Movimientos" (every charge and credit). - /estado-cuenta/[id] — the statement: balance per currency, the same balance split by business line, charges broken out by concept, and the full movement list with a running balance. - Cross-linked from the customer and property detail pages. Two data findings shape the whole module: 1. transactions.amount is a signed ledger. Every charge type is negative without exception (WATER 3115/3117, ELECTRIC 2191/2191, PROPERTY TAXES 926/926, TRUST FEE 188/188) and every deposit type positive (CHECK and CASH DEPOSIT, PAYPAL, all of EFECTIVO). So SUM(amount) is the balance and negative means the customer owes the office. 2. Currency is not summable. 912 of the 1269 customers with a ledger move in both MXN and USD, the charge side is MXN-only while receipts arrive in both, and no per-movement exchange rate was ever stored. A single "total balance" would be a figure that never existed in the books, so every total is reported per currency and the balance filter/sort takes a currency argument rather than collapsing. Also: type_transactions.nameEs is entirely null (the legacy TYPE OF TRX ESPAÑOL column is empty in all 79 rows), so Spanish concept names come from a label map in labels.ts; the entries that are payee names rather than categories fall through untranslated, which is correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
61193586a5 |
Utilities module: property browser (list/search/detail) + trust renewals
Plan step 5. Properties, services and trust accounts become a first-class
browser the way /polizas is for insurance.
API (apps/api/src/properties):
GET /properties search over address, customer, service account
number, meter, trust number and phones; filters for
service kind, municipality, trust bank, trust bucket
(with|without|active|expiring|expired|undated) and
hasServices; 5 sorts
GET /properties/stats properties/owners/services/trusts, renewal counts,
service mix per kind
GET /properties/facets kinds, municipalities, banks — all with counts
GET /properties/:id services, fideicomiso, linked policy, owner and
sibling properties, owner-level utility ledger
Web: /servicios (renewals-first browser, clickable stat cells and service-mix
strip) and /servicios/[id]. Property cards on /clientes/[id] and linked
properties on /polizas/[id] now navigate into it.
Data findings baked into the design:
- The trust deadline staff chase is trust_accounts.dueDate2 (DATMEX vence2),
one year after vence1 on 531 of 541 dated trusts: 18 due within 30 days,
119 already overdue. Every renewal bucket keys off dueDate2 alone.
- properties.zone is dead (1444 of 1519 null, the rest near-unique), so the
geographic filter is the municipality carried in the predial service's
notes (ROSARITO 566 / TIJUANA 221 / ENSENADA 152, 939/939 populated).
- PropertyService.notes means a different thing per kind (municipality, CFE
PAR/IMPAR cycle, gas supply type, cable provider) and is labelled as such.
- 240 of 1519 properties have no service rows at all — its own bucket.
Sorting by trust due date scopes to properties that have a trust, since MySQL
would otherwise float the ~966 trust-less NULLs above every real due date;
the sort label and the result meta both say so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e2aba8bd17 |
Insurance module: policy browser (list/search/detail) + renewals view
Plan step 4. Adds the policies API and the Spanish-first /polizas pages on top of the customer records the customer module already exposes. API (apps/api/src/policies): - GET /policies — search over policy number, customer name, agent, vehicle license plate, insured-driver name and legacy id; filters for vigencia bucket, ramo, aseguradora and liquidation state; five sort orders. - GET /policies/stats — bucket counts plus premium in force split by currency (MXN and USD can't be summed). - GET /policies/facets — ramos/aseguradoras with counts for the dropdowns. - GET /policies/:id — full policy plus the owning customer. Vigencia is derived from policyTo as active/expiring/expired/undated. "undated" is a real bucket rather than an error case: 528 of the 2378 migrated policies carry no end date at all. Web: - /polizas — renewals-first browser; the stat cells double as vigencia filters, with a secondary row for ramo, aseguradora and sort order. - /polizas/[id] — vigencia hero, condiciones y primas, pagos, vehículos, asegurados/beneficiarios, siniestros, the verbatim legacy coverage columns, and documents. - Nav gains Clientes | Pólizas with a real active state, and the two modules cross-link in both directions. Also fixes a display bug on the customer detail page: it headlined policies.total, which is dead data — only 2 of 2378 rows are non-zero (1585 are literally 0, 791 null), and one of those two is lower than its own net premium. That rendered "$0.00 Total" on 1585 policies. Premium headlines and the premium sort now use netPremium (2377/2378 populated); total is shown only where it is non-zero, as raw source data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9bbc077129 |
Customers: sort nameless records last instead of first
The 44 customers with no recoverable name render as "(SIN NOMBRE)", and
ordering the list by name alone floated all of them to the top — "(" sorts
before every letter — so the first two screens of the customer browser were
nothing but placeholders. Small number, worst possible position.
Adds customers.nameMissing, set by the transform and used as the primary sort
key so those records land at the end of the list. Denormalized rather than
computed in the query because the list is paginated in SQL, so the ordering
has to be expressible as a column.
Applied to the dev DB as an ALTER + UPDATE in place (no truncate), so the
existing loaded data and its FKs were left alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
594ee7cfca |
Migration: recover blank customer names from secondary legacy tables
DATGRAL.NOMBRE is blank on 266 legacy rows (140 utilities, 126 insurance), which surfaced in the UI as 257 customers literally named "(SIN NOMBRE)". The blank is real — those cells are empty in the Access files, not lost in extraction — but the rows mostly are not junk: 176 of the 257 carry a property, a policy, or transactions. The old PHP importer handled this by skipping blank-name rows outright (jorgecuadros-intra-webapp/src/tools/customerAdapter.php:47,81). That was worse than it looks: every other adapter resolved its customer FK through the customer_mapping table those skipped rows never entered, so their properties and policies were silently dropped (customerServiceAdapter.php:45) and their transactions were written against customer_id 0 (customerBalanceAdapter.php:52). So: recover the name instead of skipping. Names come from the secondary tables that still carry them, most trustworthy first — UTILSEG (the office's own hand-maintained name <-> id cross-reference spanning both lines), then the billing runs (IVA 2015, COBRO3) and the policy rows' NOMBRE ASEG (MULT, M EMPR, INCENDIO). A linked customer can also borrow the name its insurance record resolved to. Result: 213 of 257 recovered, 44 still genuinely nameless anywhere in the source. customers.nameSource records which table each recovered name came from, so a reconstructed name is never mistaken for one that was really on the record — the list tags it "nombre recuperado", the detail header names the source, and a still-unnamed customer renders muted italic instead of as a normal name. Also fixes run_all.py: transform_properties and transform_policies truncate service_documents/policy_documents, but blob_extract.py was not in the step list, so a full re-run left the uploaded MinIO objects with no rows pointing at them. Hit exactly that while reloading for this change. Verified end-to-end: full pipeline re-run against dev reproduces every prior count (1682 customers, 1519 properties, 2378 policies, 45861 transactions, 22354 bank rows, 70 documents) with zero orphans, and both apps build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
98f5cc20d8 |
Backend: Customer module (list/search/detail) + working session auth + pnpm
Adds the unified Customer API against the migrated data: - customers.service: list (search across name/email/phone/city/legacy id, business-line filter, pagination + per-row _count flags), detail (identity + legacyRefs + properties/services/trust + policies with installments/vehicles/ drivers/beneficiaries/claims/docs + recent transactions + a per-domain/ currency ledger summary), and stats. - customers.controller: GET /customers, /customers/:id, /customers/stats, guarded by AuthenticatedGuard. Registered in AppModule. - Fix LocalAuthGuard to call super.logIn so a session is actually established (login previously succeeded but persisted no session -> 403 afterwards). - apps/api/scripts/seed-user.mjs: idempotent Argon2 admin seed. Tooling: adopt pnpm as the package manager (machine npm is a pnpm shim that ignores the workspaces field). Add pnpm-workspace.yaml (+ onlyBuiltDependencies for argon2/prisma/nest native builds), switch the api's @jorgecuadros/database dep to workspace:*, add @types/passport, track pnpm-lock.yaml, drop the stale package-lock.json. Verified end-to-end against the dev DB: login sets a session cookie; stats returns 1682 customers / 526 both-lines / 45861 transactions; search + detail return the full cross-line customer view (properties+services AND policies AND a unified transaction statement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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. |