Commit Graph
5 Commits
Author SHA1 Message Date
rmancinasandClaude Opus 5 7df928c3ab feat(billing): receipt capture — outstanding workflow, batch by check, reconciliation
Build and Push Images / Build jorgecuadros-web (push) Successful in 2m1s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m3s
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>
2026-07-27 21:54:41 -07:00
rmancinas 8802f08d4f feat(reports): reports module + /inicio + edo-cuenta-datos prefill
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m35s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m11s
- 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.
2026-07-23 23:20:41 -07:00
rmancinasandClaude Opus 4.8 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>
2026-07-23 17:17:47 -07:00
rmancinasandClaude Opus 4.8 d9f9e8a920 fix(web): stop large balances clipping on the statement page
TRASPASOS PAYPAL's 7-figure balances exposed three layout bugs on
/estado-cuenta/[id], all invisible on normal small-figure customers:

- summary/línea cards: `summary-grid` uses auto-fill, so a single- or
  two-currency card never widens past the min track (~190px) however wide
  the page is. The 24px nowrap headline (-$7,028,533.44) overflowed the
  card border. Widen the min track to 260px so the figure fits in full,
  and drop the size to 22px. Explicitly no ellipsis — a truncated money
  figure reads as a wrong number.
- `concept-list` had no horizontal padding (`.card` carries none), so the
  concept totals sat flush on the card border. Pad it.
- the Cargo/Abono direction label was an inline span glued to the amount;
  make it a block so it drops onto its own line. Same fix applied to the
  movement-list page for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:41:06 -07:00
rmancinasandClaude Opus 4.8 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>
2026-07-22 23:29:19 -07:00