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>
This commit is contained in:
2026-07-23 17:17:47 -07:00
co-authored by Claude Opus 4.8
parent 548eeb5798
commit 7d9f59e51b
7 changed files with 897 additions and 36 deletions
+35
View File
@@ -16,6 +16,8 @@ import type {
BillingFacets,
BillingStats,
BusinessLine,
CreateBankMovementInput,
CreateMovementInput,
CustomerDetail,
CustomerInput,
CustomerListResponse,
@@ -43,6 +45,7 @@ import type {
Role,
ServiceKind,
Statement,
Transaction,
TransactionDomain,
TrustFilter,
UserRow,
@@ -477,6 +480,22 @@ export function getStatement(customerId: string): Promise<Statement> {
return apiFetch<Statement>(`/billing/customers/${customerId}`);
}
/** Append a new ledger movement. Booked movements are never edited — fix
* mistakes with voidMovement + a fresh capture. */
export function createMovement(
input: CreateMovementInput,
): Promise<Transaction> {
return apiFetch<Transaction>("/billing", {
method: "POST",
body: JSON.stringify(input),
});
}
/** Reverse a movement by marking it voided; totals drop it. 400 if already void. */
export function voidMovement(id: string): Promise<Transaction> {
return apiFetch<Transaction>(`/billing/${id}/void`, { method: "POST" });
}
/* ------------------------------------------------- Bank register (chequera) */
export interface BankQuery {
@@ -517,6 +536,22 @@ export function getBankSummary(year?: number): Promise<BankSummary> {
return apiFetch<BankSummary>(`/bank/summary${year ? `?year=${year}` : ""}`);
}
/** Append a new chequera movement. Booked rows are never edited — fix mistakes
* with voidBankMovement + a fresh capture. */
export function createBankMovement(
input: CreateBankMovementInput,
): Promise<unknown> {
return apiFetch("/bank", {
method: "POST",
body: JSON.stringify(input),
});
}
/** Reverse a chequera movement by marking it voided; totals drop it. */
export function voidBankMovement(id: string): Promise<unknown> {
return apiFetch(`/bank/${id}/void`, { method: "POST" });
}
/* ------------------------------------------------- Users / administration */
export function listUsers(): Promise<UserRow[]> {
+33
View File
@@ -683,6 +683,23 @@ export interface Movement {
/** Legacy table the row came from — `datos2`, `EFECTIVO`, `fee15`, … */
source: string | null;
type: TransactionType | null;
/** App-voided (`voidedAt` set). UI strikes; totals exclude. */
voided: boolean;
}
/** Payload for POST /billing — a new ledger movement. Sign convention: negative
* = cargo (charge), positive = abono (credit). */
export interface CreateMovementInput {
customerId: string;
domain: TransactionDomain;
amount: number;
transactionDate: string;
currency?: Currency;
typeId?: string;
period?: string;
reference?: string;
checkNumber?: string;
message?: string;
}
export interface MovementListItem extends Movement {
@@ -909,6 +926,22 @@ export interface BankListItem {
/** "CIENTO CINCUENTA MIL PESOS 00/100" — egresos only. */
amountInWords: string | null;
source: string | null;
/** App-voided (`voidedAt` set). UI strikes; totals exclude. */
voided: boolean;
}
/** Payload for POST /bank — a new chequera movement. Sign convention: positive
* = ingreso, negative = egreso. MXN only. */
export interface CreateBankMovementInput {
amount: number;
transactionDate: string;
concept?: string;
reference?: string;
transactionType?: string;
cleared?: boolean;
transferred?: boolean;
notes?: string;
amountInWords?: string;
}
export interface BankTotals {