Commit Graph
4 Commits
Author SHA1 Message Date
rmancinasandClaude Opus 4.8 afe2411c86 feat(storage): wire MinIO/S3 document upload & download into the API + web
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m8s
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m37s
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>
2026-07-23 19:19:57 -07:00
rmancinasandClaude Opus 4.8 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>
2026-07-23 12:34:47 -07:00
rmancinasandClaude Opus 4.8 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>
2026-07-23 12:27:53 -07:00
rmancinasandClaude Opus 4.8 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>
2026-07-22 22:04:07 -07:00