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>
This commit is contained in:
2026-07-23 12:22:02 -07:00
co-authored by Claude Opus 4.8
parent 12692a0af8
commit 7a46c30d9b
22 changed files with 1973 additions and 19 deletions
+79
View File
@@ -26,10 +26,12 @@ import type {
MovementSort,
PolicyDetail,
PolicyFacets,
PolicyInput,
PolicyListResponse,
PolicySort,
PolicyStats,
PolicyStatus,
LookupsResponse,
PropertyDetail,
PropertyFacets,
PropertyListResponse,
@@ -208,6 +210,83 @@ export function getPolicy(
return apiFetch<PolicyDetail>(`/policies/${id}?days=${days}`);
}
export function createPolicy(input: PolicyInput): Promise<PolicyDetail> {
return apiFetch<PolicyDetail>("/policies", {
method: "POST",
body: JSON.stringify(input),
});
}
export function updatePolicy(
id: string,
input: Partial<PolicyInput>,
): Promise<PolicyDetail> {
return apiFetch<PolicyDetail>(`/policies/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function archivePolicy(id: string): Promise<PolicyDetail> {
return apiFetch<PolicyDetail>(`/policies/${id}`, { method: "DELETE" });
}
export function restorePolicy(id: string): Promise<PolicyDetail> {
return apiFetch<PolicyDetail>(`/policies/${id}/restore`, { method: "POST" });
}
// Generic policy-child CRUD. `kind` is the URL segment
// (installments|vehicles|drivers|beneficiaries|claims).
export function addPolicyChild<T>(
policyId: string,
kind: string,
input: T,
): Promise<unknown> {
return apiFetch(`/policies/${policyId}/${kind}`, {
method: "POST",
body: JSON.stringify(input),
});
}
export function updatePolicyChild<T>(
policyId: string,
kind: string,
childId: string,
input: T,
): Promise<unknown> {
return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function removePolicyChild(
policyId: string,
kind: string,
childId: string,
): Promise<unknown> {
return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
method: "DELETE",
});
}
/* ------------------------------------------------- Lookups (insurance ref) */
export function getLookups(): Promise<LookupsResponse> {
return apiFetch<LookupsResponse>("/lookups");
}
export function createLookup(kind: string, input: unknown): Promise<unknown> {
return apiFetch(`/lookups/${kind}`, { method: "POST", body: JSON.stringify(input) });
}
export function updateLookup(
kind: string,
id: string,
input: unknown,
): Promise<unknown> {
return apiFetch(`/lookups/${kind}/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function removeLookup(kind: string, id: string): Promise<unknown> {
return apiFetch(`/lookups/${kind}/${id}`, { method: "DELETE" });
}
/* ----------------------------------------------------- Utilities module */
export interface PropertyQuery {
+112 -1
View File
@@ -155,17 +155,23 @@ export interface Vehicle {
id: string;
make: string | null;
model: string | null;
modelYear: number | null;
modelYear: number | string | null;
licensePlate: string | null;
bodyType: string | null;
engineNumber?: string | null;
vinNumber?: string | null;
stateCode?: string | null;
notes?: string | null;
}
export interface InsuredDriver {
id: string;
fullName: string | null;
licenseNumber: string | null;
birthDate?: string | null;
sex?: string | null;
occupation?: string | null;
licenseState?: string | null;
}
export interface Beneficiary {
@@ -237,6 +243,106 @@ export interface PolicyListItem {
vehicleCount: number;
installmentCount: number;
documentCount: number;
archived: boolean;
}
/** Editable policy-header fields — shared by the create/edit form and API. */
export interface PolicyInput {
policyNumber: string;
customerId: string;
policyTypeId?: string;
insuranceProviderId?: string;
agentName?: string;
policyDate?: string;
policyFrom?: string;
policyTo?: string;
coveragePeriodDays?: number;
netPremium?: number;
policyFee?: number;
brokerFee?: number;
commission?: number;
total?: number;
currency?: Currency;
observations?: string;
notes?: string;
endorsement?: boolean;
liquidated?: boolean;
liquidationNumber?: string;
liquidationDate?: string;
}
export interface InstallmentInput {
sequence: number;
amount?: number;
currency?: Currency;
dueDate?: string;
paidDate?: string;
checkNumber?: string;
isCash?: boolean;
}
export interface VehicleInput {
make?: string;
model?: string;
modelYear?: string;
bodyType?: string;
engineNumber?: string;
licensePlate?: string;
vinNumber?: string;
stateCode?: string;
notes?: string;
}
export interface DriverInput {
fullName?: string;
birthDate?: string;
sex?: string;
occupation?: string;
licenseNumber?: string;
licenseState?: string;
}
export interface BeneficiaryInput {
name?: string;
address?: string;
phone?: string;
email?: string;
}
export interface ClaimInput {
claimType?: string;
incidentDate?: string;
reportedDate?: string;
description?: string;
adjusterId?: string;
claimedAmount?: number;
settledAmount?: number;
settlementDate?: string;
checkNumber?: string;
resolved?: boolean;
resolutionNotes?: string;
}
/* Lookups (insurance reference data) */
export interface ProviderRow {
id: string;
name: string;
_count?: { policies: number };
}
export interface PolicyTypeRow {
id: string;
name: string;
shortDescription: string | null;
_count?: { policies: number };
}
export interface AdjusterRow {
id: string;
company: string | null;
city: string | null;
name: string | null;
phone: string | null;
beeper: string | null;
}
export interface LookupsResponse {
providers: ProviderRow[];
types: PolicyTypeRow[];
adjusters: AdjusterRow[];
}
export interface PolicyListResponse {
@@ -295,6 +401,10 @@ export interface Claim {
settlementDate: string | null;
status?: string | null;
adjuster: Adjuster | null;
adjusterId?: string | null;
checkNumber?: string | null;
resolved?: boolean;
resolutionNotes?: string | null;
}
export interface PolicyCustomerRef {
@@ -333,6 +443,7 @@ export interface PolicyDetail {
legacySourceDb: string | null;
legacySourceTable: string | null;
legacyId: string | null;
archivedAt: string | null;
status: PolicyStatus;
daysToExpiry: number | null;
customer: PolicyCustomerRef;