36158ae76178949974903e67a1f44f07dc9e2123
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f9a9afc0d |
fix(billing): the cash receipt book is not a second ledger
EFECTIVO is a journal, not a ledger. The office writes a numbered paper receipt for money handed over the counter and then posts that same receipt to the utilities ledger as reference `C<folio>`. Legacy summed the ledger alone — ledger_repository.php reads `datosfreak`, materialized from DATOS2 only — but the migration flattened both tables into one `transactions` table, so every balance counted each counter payment twice. Confirmed against the live legacy database rather than inferred: of the 297 receipts written in 2026, 296 carry a matching DATOS2 posting. Six of them post converted to pesos under a mistyped folio, which is why matching pairs on folio and amount found fewer duplicates than exist — and why this excludes the whole journal instead of a list of confirmed pairs. Only folio 13536 (CL 717, $400 USD) has no posting anywhere; that one wants a human. The database qualifier is load-bearing. `SEGUROS 16_be` keeps its own table also called EFECTIVO, and that one is the insurance line's only ledger — nothing posts it anywhere else. Excluding by table name alone would erase 55,444.95 USD and 63,957.78 MXN across 102 customers, 99 of whom have no other rows at all. Extending the qualified rule to the statement and the customer file also gives those 99 back a statement that is not empty. The same queries were missing the archive window the statement already had, so the worklist and the book also counted a closed year twice for customers floored inside an archive. Measured on production, utilities MXN: NUMid 6 and 173 unchanged to the cent, 501 unchanged at -10,874.33 (still the portal's number), 10 drops 2,362.20 -> -1,137.80 (exactly the 3,500.00 duplicate), 295 drops 14,377.46 -> 4,377.46 — which is what his statement already said. The worklist and the statement now agree, which is the point. Left alone deliberately: the movement browser, which is inventory rather than balance and should still show what was captured; and stats()'s outstanding rows, which turn on a client decision that is still open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aa5867c8ea |
fix(statements): an archive is history below the year start, not nothing
|
||
|
|
c6feae9522 |
fix(customers): the customer file's balances are the statement's balances
The /clientes/:id ledger card is titled "Estado de cuenta" and links straight to
the statement, but its per-line tiles came from a groupBy with `voidedAt: null`
and nothing else — no balance floor, no source exclusion, no outstanding rule.
It was a raw lifetime sum, double-counting the pre-cutover history that each
BALANCE FORWARD row already absorbs, and the card said so in its own footnote
rather than being fixed.
Importing prior periods turned that from wrong into badly wrong. Every closed
year is now also held as its own tagged copy, so an unfloored sum adds each one
a second time on top of the opening balance that contains it. NUMid 501 read
-7,119.29 before the archives landed and -15,270.59 after, against a true
-10,715.29 — the gap being exactly the 2024 and 2025 closing balances.
The tiles now take the same three rules the statement takes, and agree with it
for all 600 customers sampled.
Two places needed the archives excluded explicitly, because the balance floor
does not do it:
- A customer whose newest BALANCE FORWARD lives inside an archive floors at
that archive's own January 1, so every row of it clears the floor. One
customer, 28 rows.
- The archives are not cleanly bounded. datos2@2024 carries rows dated 2022,
2023, 2025 and one in 2026; datos2@2025 two more. Those clear any floor and
land in the current year next to the live ledger's own copy of them.
That second point is a defect in
|
||
|
|
9929a9a3ac |
feat(customers): the customer file's estado de cuenta follows the same year rule
`/clientes/:id` carries its own "Estado de cuenta" card, fed by CustomersService.detail rather than by BillingService.statement, so the previous commit left it reading the old way: the last 100 movements of all time, newest first. Two pages, one label, two orders. It now covers the current calendar year oldest-first, like the statement and like the sheet the office prints. The `take: 100` is gone with it — capping a descending list hid the oldest rows, but capping an ascending one would hide the newest, and a single year is small (365 rows for the heaviest customer in the book). The per-domain totals above the list are untouched and still historical: they are an unfloored groupBy, so they double-count every customer's opening balance. That is a separate bug from this one; the card now says out loud that those figures are lifetime, not this year's. Co-Authored-By: Claude Opus 5 <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> |
||
|
|
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> |
||
|
|
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> |