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:
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator";
|
||||
import { Currency } from "@jorgecuadros/database";
|
||||
|
||||
// Each child DTO covers create; updates reuse the same shape with all fields
|
||||
// optional via the corresponding Update class. Route supplies the policyId.
|
||||
|
||||
export class InstallmentDto {
|
||||
@IsInt() sequence!: number;
|
||||
@IsOptional() @IsNumber() amount?: number;
|
||||
@IsOptional() @IsEnum(Currency) currency?: Currency;
|
||||
@IsOptional() @IsString() dueDate?: string;
|
||||
@IsOptional() @IsString() paidDate?: string;
|
||||
@IsOptional() @IsString() checkNumber?: string;
|
||||
@IsOptional() @IsBoolean() isCash?: boolean;
|
||||
}
|
||||
export class UpdateInstallmentDto {
|
||||
@IsOptional() @IsInt() sequence?: number;
|
||||
@IsOptional() @IsNumber() amount?: number;
|
||||
@IsOptional() @IsEnum(Currency) currency?: Currency;
|
||||
@IsOptional() @IsString() dueDate?: string;
|
||||
@IsOptional() @IsString() paidDate?: string;
|
||||
@IsOptional() @IsString() checkNumber?: string;
|
||||
@IsOptional() @IsBoolean() isCash?: boolean;
|
||||
}
|
||||
|
||||
export class VehicleDto {
|
||||
@IsOptional() @IsString() make?: string;
|
||||
@IsOptional() @IsString() model?: string;
|
||||
@IsOptional() @IsString() modelYear?: string;
|
||||
@IsOptional() @IsString() bodyType?: string;
|
||||
@IsOptional() @IsString() engineNumber?: string;
|
||||
@IsOptional() @IsString() licensePlate?: string;
|
||||
@IsOptional() @IsString() vinNumber?: string;
|
||||
@IsOptional() @IsString() stateCode?: string;
|
||||
@IsOptional() @IsString() notes?: string;
|
||||
}
|
||||
export class UpdateVehicleDto extends VehicleDto {}
|
||||
|
||||
export class DriverDto {
|
||||
@IsOptional() @IsString() fullName?: string;
|
||||
@IsOptional() @IsString() birthDate?: string;
|
||||
@IsOptional() @IsString() sex?: string;
|
||||
@IsOptional() @IsString() occupation?: string;
|
||||
@IsOptional() @IsString() licenseNumber?: string;
|
||||
@IsOptional() @IsString() licenseState?: string;
|
||||
}
|
||||
export class UpdateDriverDto extends DriverDto {}
|
||||
|
||||
export class BeneficiaryDto {
|
||||
@IsOptional() @IsString() name?: string;
|
||||
@IsOptional() @IsString() address?: string;
|
||||
@IsOptional() @IsString() phone?: string;
|
||||
@IsOptional() @IsEmail() email?: string;
|
||||
}
|
||||
export class UpdateBeneficiaryDto extends BeneficiaryDto {}
|
||||
|
||||
export class ClaimDto {
|
||||
@IsOptional() @IsString() claimType?: string;
|
||||
@IsOptional() @IsString() incidentDate?: string;
|
||||
@IsOptional() @IsString() reportedDate?: string;
|
||||
@IsOptional() @IsString() description?: string;
|
||||
@IsOptional() @IsString() adjusterId?: string;
|
||||
@IsOptional() @IsNumber() claimedAmount?: number;
|
||||
@IsOptional() @IsNumber() settledAmount?: number;
|
||||
@IsOptional() @IsString() settlementDate?: string;
|
||||
@IsOptional() @IsString() checkNumber?: string;
|
||||
@IsOptional() @IsBoolean() resolved?: boolean;
|
||||
@IsOptional() @IsString() resolutionNotes?: string;
|
||||
}
|
||||
export class UpdateClaimDto extends ClaimDto {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IsOptional, IsString, MinLength } from "class-validator";
|
||||
|
||||
export class ProviderDto {
|
||||
@IsString() @MinLength(1) name!: string;
|
||||
}
|
||||
export class UpdateProviderDto {
|
||||
@IsOptional() @IsString() @MinLength(1) name?: string;
|
||||
}
|
||||
|
||||
export class PolicyTypeDto {
|
||||
@IsString() @MinLength(1) name!: string;
|
||||
@IsOptional() @IsString() shortDescription?: string;
|
||||
}
|
||||
export class UpdatePolicyTypeDto {
|
||||
@IsOptional() @IsString() @MinLength(1) name?: string;
|
||||
@IsOptional() @IsString() shortDescription?: string;
|
||||
}
|
||||
|
||||
export class AdjusterDto {
|
||||
@IsOptional() @IsString() company?: string;
|
||||
@IsOptional() @IsString() city?: string;
|
||||
@IsOptional() @IsString() name?: string;
|
||||
@IsOptional() @IsString() phone?: string;
|
||||
@IsOptional() @IsString() beeper?: string;
|
||||
}
|
||||
export class UpdateAdjusterDto extends AdjusterDto {}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||||
import { AbilityGuard } from "../auth/ability.guard";
|
||||
import { RequireAbility } from "../auth/require-ability.decorator";
|
||||
import { PoliciesService } from "./policies.service";
|
||||
import {
|
||||
AdjusterDto,
|
||||
PolicyTypeDto,
|
||||
ProviderDto,
|
||||
UpdateAdjusterDto,
|
||||
UpdatePolicyTypeDto,
|
||||
UpdateProviderDto,
|
||||
} from "./lookup.dto";
|
||||
|
||||
/**
|
||||
* Insurance reference data: providers, policy types, adjusters. Reading is open
|
||||
* to any authenticated user (the policy form needs the options); mutating needs
|
||||
* "lookup:manage" (MANAGER+).
|
||||
*/
|
||||
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
||||
@Controller("lookups")
|
||||
export class LookupsController {
|
||||
constructor(private readonly policies: PoliciesService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.policies.listLookups();
|
||||
}
|
||||
|
||||
@Post("providers")
|
||||
@RequireAbility("lookup:manage")
|
||||
createProvider(@Body() dto: ProviderDto) {
|
||||
return this.policies.createProvider(dto);
|
||||
}
|
||||
@Patch("providers/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
updateProvider(@Param("id") id: string, @Body() dto: UpdateProviderDto) {
|
||||
return this.policies.updateProvider(id, dto);
|
||||
}
|
||||
@Delete("providers/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
removeProvider(@Param("id") id: string) {
|
||||
return this.policies.removeProvider(id);
|
||||
}
|
||||
|
||||
@Post("policy-types")
|
||||
@RequireAbility("lookup:manage")
|
||||
createType(@Body() dto: PolicyTypeDto) {
|
||||
return this.policies.createPolicyType(dto);
|
||||
}
|
||||
@Patch("policy-types/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
updateType(@Param("id") id: string, @Body() dto: UpdatePolicyTypeDto) {
|
||||
return this.policies.updatePolicyType(id, dto);
|
||||
}
|
||||
@Delete("policy-types/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
removeType(@Param("id") id: string) {
|
||||
return this.policies.removePolicyType(id);
|
||||
}
|
||||
|
||||
@Post("adjusters")
|
||||
@RequireAbility("lookup:manage")
|
||||
createAdjuster(@Body() dto: AdjusterDto) {
|
||||
return this.policies.createAdjuster(dto);
|
||||
}
|
||||
@Patch("adjusters/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
updateAdjuster(@Param("id") id: string, @Body() dto: UpdateAdjusterDto) {
|
||||
return this.policies.updateAdjuster(id, dto);
|
||||
}
|
||||
@Delete("adjusters/:id")
|
||||
@RequireAbility("lookup:manage")
|
||||
removeAdjuster(@Param("id") id: string) {
|
||||
return this.policies.removeAdjuster(id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,37 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from "@nestjs/common";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||||
import { AbilityGuard } from "../auth/ability.guard";
|
||||
import { RequireAbility } from "../auth/require-ability.decorator";
|
||||
import { AuditService } from "../common/audit.service";
|
||||
import {
|
||||
PoliciesService,
|
||||
type PolicySort,
|
||||
type PolicyStatus,
|
||||
} from "./policies.service";
|
||||
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
||||
import {
|
||||
BeneficiaryDto,
|
||||
ClaimDto,
|
||||
DriverDto,
|
||||
InstallmentDto,
|
||||
UpdateBeneficiaryDto,
|
||||
UpdateClaimDto,
|
||||
UpdateDriverDto,
|
||||
UpdateInstallmentDto,
|
||||
VehicleDto,
|
||||
} from "./children.dto";
|
||||
|
||||
const STATUSES: PolicyStatus[] = ["active", "expiring", "expired", "undated"];
|
||||
const SORTS: PolicySort[] = [
|
||||
@@ -15,15 +42,21 @@ const SORTS: PolicySort[] = [
|
||||
"premium_desc",
|
||||
];
|
||||
|
||||
/** Clamped expiry window; 30 days is the default renewal horizon. */
|
||||
function parseDays(days?: string): number {
|
||||
return Math.min(365, Math.max(1, Number(days) || 30));
|
||||
}
|
||||
|
||||
@UseGuards(AuthenticatedGuard)
|
||||
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
||||
@Controller("policies")
|
||||
export class PoliciesController {
|
||||
constructor(private readonly policies: PoliciesService) {}
|
||||
constructor(
|
||||
private readonly policies: PoliciesService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
private actingId(req: Request): string {
|
||||
return (req.user as { id: string }).id;
|
||||
}
|
||||
|
||||
@Get("stats")
|
||||
stats(@Query("days") days?: string) {
|
||||
@@ -45,6 +78,7 @@ export class PoliciesController {
|
||||
@Query("typeId") typeId?: string,
|
||||
@Query("providerId") providerId?: string,
|
||||
@Query("liquidated") liquidated?: string,
|
||||
@Query("includeArchived") includeArchived?: string,
|
||||
@Query("sort") sort?: string,
|
||||
) {
|
||||
return this.policies.list({
|
||||
@@ -59,6 +93,7 @@ export class PoliciesController {
|
||||
providerId: providerId || undefined,
|
||||
liquidated:
|
||||
liquidated === "true" ? true : liquidated === "false" ? false : undefined,
|
||||
includeArchived: includeArchived === "true",
|
||||
sort: SORTS.includes(sort as PolicySort)
|
||||
? (sort as PolicySort)
|
||||
: "expiry_desc",
|
||||
@@ -69,4 +104,140 @@ export class PoliciesController {
|
||||
detail(@Param("id") id: string, @Query("days") days?: string) {
|
||||
return this.policies.detail(id, parseDays(days));
|
||||
}
|
||||
|
||||
// --- header writes --------------------------------------------------------
|
||||
|
||||
@Post()
|
||||
@RequireAbility("policy:create")
|
||||
async create(@Body() dto: CreatePolicyDto, @Req() req: Request) {
|
||||
const p = await this.policies.create(dto);
|
||||
void this.audit.log(this.actingId(req), "policy.create", { policyId: p.id });
|
||||
return p;
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@RequireAbility("policy:update")
|
||||
async update(@Param("id") id: string, @Body() dto: UpdatePolicyDto, @Req() req: Request) {
|
||||
const p = await this.policies.update(id, dto);
|
||||
void this.audit.log(this.actingId(req), "policy.update", { policyId: id });
|
||||
return p;
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@RequireAbility("policy:delete")
|
||||
async archive(@Param("id") id: string, @Req() req: Request) {
|
||||
const p = await this.policies.archive(id);
|
||||
void this.audit.log(this.actingId(req), "policy.archive", { policyId: id });
|
||||
return p;
|
||||
}
|
||||
|
||||
@Post(":id/restore")
|
||||
@RequireAbility("policy:delete")
|
||||
async restore(@Param("id") id: string, @Req() req: Request) {
|
||||
const p = await this.policies.restore(id);
|
||||
void this.audit.log(this.actingId(req), "policy.restore", { policyId: id });
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- children (all editing a policy => policy:update) ---------------------
|
||||
|
||||
@Post(":id/installments")
|
||||
@RequireAbility("policy:update")
|
||||
addInstallment(@Param("id") id: string, @Body() dto: InstallmentDto) {
|
||||
return this.policies.addInstallment(id, dto);
|
||||
}
|
||||
@Patch(":id/installments/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
updateInstallment(
|
||||
@Param("id") id: string,
|
||||
@Param("childId") childId: string,
|
||||
@Body() dto: UpdateInstallmentDto,
|
||||
) {
|
||||
return this.policies.updateInstallment(id, childId, dto);
|
||||
}
|
||||
@Delete(":id/installments/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
removeInstallment(@Param("id") id: string, @Param("childId") childId: string) {
|
||||
return this.policies.removeInstallment(id, childId);
|
||||
}
|
||||
|
||||
@Post(":id/vehicles")
|
||||
@RequireAbility("policy:update")
|
||||
addVehicle(@Param("id") id: string, @Body() dto: VehicleDto) {
|
||||
return this.policies.addVehicle(id, dto);
|
||||
}
|
||||
@Patch(":id/vehicles/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
updateVehicle(
|
||||
@Param("id") id: string,
|
||||
@Param("childId") childId: string,
|
||||
@Body() dto: VehicleDto,
|
||||
) {
|
||||
return this.policies.updateVehicle(id, childId, dto);
|
||||
}
|
||||
@Delete(":id/vehicles/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
removeVehicle(@Param("id") id: string, @Param("childId") childId: string) {
|
||||
return this.policies.removeVehicle(id, childId);
|
||||
}
|
||||
|
||||
@Post(":id/drivers")
|
||||
@RequireAbility("policy:update")
|
||||
addDriver(@Param("id") id: string, @Body() dto: DriverDto) {
|
||||
return this.policies.addDriver(id, dto);
|
||||
}
|
||||
@Patch(":id/drivers/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
updateDriver(
|
||||
@Param("id") id: string,
|
||||
@Param("childId") childId: string,
|
||||
@Body() dto: UpdateDriverDto,
|
||||
) {
|
||||
return this.policies.updateDriver(id, childId, dto);
|
||||
}
|
||||
@Delete(":id/drivers/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
removeDriver(@Param("id") id: string, @Param("childId") childId: string) {
|
||||
return this.policies.removeDriver(id, childId);
|
||||
}
|
||||
|
||||
@Post(":id/beneficiaries")
|
||||
@RequireAbility("policy:update")
|
||||
addBeneficiary(@Param("id") id: string, @Body() dto: BeneficiaryDto) {
|
||||
return this.policies.addBeneficiary(id, dto);
|
||||
}
|
||||
@Patch(":id/beneficiaries/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
updateBeneficiary(
|
||||
@Param("id") id: string,
|
||||
@Param("childId") childId: string,
|
||||
@Body() dto: UpdateBeneficiaryDto,
|
||||
) {
|
||||
return this.policies.updateBeneficiary(id, childId, dto);
|
||||
}
|
||||
@Delete(":id/beneficiaries/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
removeBeneficiary(@Param("id") id: string, @Param("childId") childId: string) {
|
||||
return this.policies.removeBeneficiary(id, childId);
|
||||
}
|
||||
|
||||
@Post(":id/claims")
|
||||
@RequireAbility("policy:update")
|
||||
addClaim(@Param("id") id: string, @Body() dto: ClaimDto) {
|
||||
return this.policies.addClaim(id, dto);
|
||||
}
|
||||
@Patch(":id/claims/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
updateClaim(
|
||||
@Param("id") id: string,
|
||||
@Param("childId") childId: string,
|
||||
@Body() dto: UpdateClaimDto,
|
||||
) {
|
||||
return this.policies.updateClaim(id, childId, dto);
|
||||
}
|
||||
@Delete(":id/claims/:childId")
|
||||
@RequireAbility("policy:update")
|
||||
removeClaim(@Param("id") id: string, @Param("childId") childId: string) {
|
||||
return this.policies.removeClaim(id, childId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PoliciesController } from "./policies.controller";
|
||||
import { LookupsController } from "./lookups.controller";
|
||||
import { PoliciesService } from "./policies.service";
|
||||
|
||||
@Module({
|
||||
controllers: [PoliciesController],
|
||||
controllers: [PoliciesController, LookupsController],
|
||||
providers: [PoliciesService],
|
||||
})
|
||||
export class PoliciesModule {}
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma } from "@jorgecuadros/database";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { toDate } from "../common/coerce";
|
||||
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
||||
import {
|
||||
BeneficiaryDto,
|
||||
ClaimDto,
|
||||
DriverDto,
|
||||
InstallmentDto,
|
||||
UpdateBeneficiaryDto,
|
||||
UpdateClaimDto,
|
||||
UpdateDriverDto,
|
||||
UpdateInstallmentDto,
|
||||
VehicleDto,
|
||||
} from "./children.dto";
|
||||
import {
|
||||
AdjusterDto,
|
||||
PolicyTypeDto,
|
||||
ProviderDto,
|
||||
UpdateAdjusterDto,
|
||||
UpdatePolicyTypeDto,
|
||||
UpdateProviderDto,
|
||||
} from "./lookup.dto";
|
||||
|
||||
/**
|
||||
* Vigencia buckets, derived from `policyTo` against today. `undated` is a real
|
||||
@@ -27,6 +48,7 @@ export interface ListParams {
|
||||
typeId?: string;
|
||||
providerId?: string;
|
||||
liquidated?: boolean;
|
||||
includeArchived?: boolean;
|
||||
sort: PolicySort;
|
||||
}
|
||||
|
||||
@@ -97,11 +119,13 @@ export class PoliciesService {
|
||||
|
||||
/** Policy list with search, vigencia/type/provider filters, paginated. */
|
||||
async list(params: ListParams) {
|
||||
const { query, page, pageSize, status, days, typeId, providerId, liquidated, sort } =
|
||||
params;
|
||||
const { query, page, pageSize, status, days, typeId, providerId, liquidated,
|
||||
includeArchived, sort } = params;
|
||||
|
||||
const where: Prisma.PolicyWhereInput = { ...this.statusWhere(status, days) };
|
||||
|
||||
if (!includeArchived) where.archivedAt = null;
|
||||
|
||||
if (query && query.trim()) {
|
||||
const q = query.trim();
|
||||
where.OR = [
|
||||
@@ -134,6 +158,7 @@ export class PoliciesService {
|
||||
total: true,
|
||||
currency: true,
|
||||
liquidated: true,
|
||||
archivedAt: true,
|
||||
customer: { select: { id: true, name: true, city: true } },
|
||||
policyType: { select: { id: true, name: true } },
|
||||
insuranceProvider: { select: { id: true, name: true } },
|
||||
@@ -155,6 +180,7 @@ export class PoliciesService {
|
||||
total: r.total,
|
||||
currency: r.currency,
|
||||
liquidated: r.liquidated,
|
||||
archived: r.archivedAt != null,
|
||||
customerId: r.customer.id,
|
||||
customerName: r.customer.name,
|
||||
customerCity: r.customer.city,
|
||||
@@ -278,4 +304,229 @@ export class PoliciesService {
|
||||
daysToExpiry: daysUntil(policy.policyTo, from),
|
||||
};
|
||||
}
|
||||
|
||||
// --- policy header writes -------------------------------------------------
|
||||
|
||||
private headerData(dto: CreatePolicyDto | UpdatePolicyDto) {
|
||||
const { policyDate, policyFrom, policyTo, liquidationDate, ...rest } =
|
||||
dto as CreatePolicyDto;
|
||||
return {
|
||||
...rest,
|
||||
...(policyDate !== undefined && { policyDate: toDate(policyDate) }),
|
||||
...(policyFrom !== undefined && { policyFrom: toDate(policyFrom) }),
|
||||
...(policyTo !== undefined && { policyTo: toDate(policyTo) }),
|
||||
...(liquidationDate !== undefined && { liquidationDate: toDate(liquidationDate) }),
|
||||
};
|
||||
}
|
||||
|
||||
async create(dto: CreatePolicyDto) {
|
||||
// Validate the customer FK up front for a clean 404 instead of a raw
|
||||
// Prisma constraint error.
|
||||
const customer = await this.prisma.customer.findUnique({
|
||||
where: { id: dto.customerId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!customer) throw new NotFoundException(`Customer ${dto.customerId} not found`);
|
||||
|
||||
return this.prisma.policy.create({
|
||||
data: {
|
||||
...this.headerData(dto),
|
||||
policyNumber: dto.policyNumber,
|
||||
customerId: dto.customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdatePolicyDto) {
|
||||
await this.ensurePolicy(id);
|
||||
return this.prisma.policy.update({ where: { id }, data: this.headerData(dto) });
|
||||
}
|
||||
|
||||
async archive(id: string) {
|
||||
await this.ensurePolicy(id);
|
||||
return this.prisma.policy.update({ where: { id }, data: { archivedAt: new Date() } });
|
||||
}
|
||||
|
||||
async restore(id: string) {
|
||||
await this.ensurePolicy(id);
|
||||
return this.prisma.policy.update({ where: { id }, data: { archivedAt: null } });
|
||||
}
|
||||
|
||||
private async ensurePolicy(id: string) {
|
||||
const found = await this.prisma.policy.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!found) throw new NotFoundException(`Policy ${id} not found`);
|
||||
}
|
||||
|
||||
// --- child rows -----------------------------------------------------------
|
||||
// Each child is created under a policy and edited/removed by its own id,
|
||||
// scoped to that policy so one policy's id can't touch another's rows.
|
||||
|
||||
private async ensureChild(
|
||||
model: "policyPaymentInstallment" | "vehicle" | "insuredDriver" | "policyBeneficiary" | "claim",
|
||||
policyId: string,
|
||||
childId: string,
|
||||
) {
|
||||
await this.ensurePolicy(policyId);
|
||||
// @ts-expect-error dynamic delegate access is safe for these known models
|
||||
const row = await this.prisma[model].findFirst({
|
||||
where: { id: childId, policyId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!row) throw new NotFoundException(`Child ${childId} not found on policy ${policyId}`);
|
||||
}
|
||||
|
||||
async addInstallment(policyId: string, dto: InstallmentDto) {
|
||||
await this.ensurePolicy(policyId);
|
||||
return this.prisma.policyPaymentInstallment.create({
|
||||
data: {
|
||||
policyId,
|
||||
sequence: dto.sequence,
|
||||
amount: dto.amount,
|
||||
currency: dto.currency,
|
||||
dueDate: toDate(dto.dueDate) ?? undefined,
|
||||
paidDate: toDate(dto.paidDate) ?? undefined,
|
||||
checkNumber: dto.checkNumber,
|
||||
isCash: dto.isCash,
|
||||
},
|
||||
});
|
||||
}
|
||||
async updateInstallment(policyId: string, id: string, dto: UpdateInstallmentDto) {
|
||||
await this.ensureChild("policyPaymentInstallment", policyId, id);
|
||||
return this.prisma.policyPaymentInstallment.update({
|
||||
where: { id },
|
||||
data: {
|
||||
sequence: dto.sequence,
|
||||
amount: dto.amount,
|
||||
currency: dto.currency,
|
||||
...(dto.dueDate !== undefined && { dueDate: toDate(dto.dueDate) }),
|
||||
...(dto.paidDate !== undefined && { paidDate: toDate(dto.paidDate) }),
|
||||
checkNumber: dto.checkNumber,
|
||||
isCash: dto.isCash,
|
||||
},
|
||||
});
|
||||
}
|
||||
async removeInstallment(policyId: string, id: string) {
|
||||
await this.ensureChild("policyPaymentInstallment", policyId, id);
|
||||
return this.prisma.policyPaymentInstallment.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async addVehicle(policyId: string, dto: VehicleDto) {
|
||||
await this.ensurePolicy(policyId);
|
||||
return this.prisma.vehicle.create({ data: { policyId, ...dto } });
|
||||
}
|
||||
async updateVehicle(policyId: string, id: string, dto: VehicleDto) {
|
||||
await this.ensureChild("vehicle", policyId, id);
|
||||
return this.prisma.vehicle.update({ where: { id }, data: { ...dto } });
|
||||
}
|
||||
async removeVehicle(policyId: string, id: string) {
|
||||
await this.ensureChild("vehicle", policyId, id);
|
||||
return this.prisma.vehicle.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async addDriver(policyId: string, dto: DriverDto) {
|
||||
await this.ensurePolicy(policyId);
|
||||
return this.prisma.insuredDriver.create({
|
||||
data: { policyId, ...dto, birthDate: toDate(dto.birthDate) ?? undefined },
|
||||
});
|
||||
}
|
||||
async updateDriver(policyId: string, id: string, dto: UpdateDriverDto) {
|
||||
await this.ensureChild("insuredDriver", policyId, id);
|
||||
return this.prisma.insuredDriver.update({
|
||||
where: { id },
|
||||
data: { ...dto, ...(dto.birthDate !== undefined && { birthDate: toDate(dto.birthDate) }) },
|
||||
});
|
||||
}
|
||||
async removeDriver(policyId: string, id: string) {
|
||||
await this.ensureChild("insuredDriver", policyId, id);
|
||||
return this.prisma.insuredDriver.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async addBeneficiary(policyId: string, dto: BeneficiaryDto) {
|
||||
await this.ensurePolicy(policyId);
|
||||
return this.prisma.policyBeneficiary.create({ data: { policyId, ...dto } });
|
||||
}
|
||||
async updateBeneficiary(policyId: string, id: string, dto: UpdateBeneficiaryDto) {
|
||||
await this.ensureChild("policyBeneficiary", policyId, id);
|
||||
return this.prisma.policyBeneficiary.update({ where: { id }, data: { ...dto } });
|
||||
}
|
||||
async removeBeneficiary(policyId: string, id: string) {
|
||||
await this.ensureChild("policyBeneficiary", policyId, id);
|
||||
return this.prisma.policyBeneficiary.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async addClaim(policyId: string, dto: ClaimDto) {
|
||||
await this.ensurePolicy(policyId);
|
||||
return this.prisma.claim.create({ data: { policyId, ...this.claimData(dto) } });
|
||||
}
|
||||
async updateClaim(policyId: string, id: string, dto: UpdateClaimDto) {
|
||||
await this.ensureChild("claim", policyId, id);
|
||||
return this.prisma.claim.update({ where: { id }, data: this.claimData(dto) });
|
||||
}
|
||||
async removeClaim(policyId: string, id: string) {
|
||||
await this.ensureChild("claim", policyId, id);
|
||||
return this.prisma.claim.delete({ where: { id } });
|
||||
}
|
||||
private claimData(dto: ClaimDto) {
|
||||
const { incidentDate, reportedDate, settlementDate, ...rest } = dto;
|
||||
return {
|
||||
...rest,
|
||||
...(incidentDate !== undefined && { incidentDate: toDate(incidentDate) }),
|
||||
...(reportedDate !== undefined && { reportedDate: toDate(reportedDate) }),
|
||||
...(settlementDate !== undefined && { settlementDate: toDate(settlementDate) }),
|
||||
};
|
||||
}
|
||||
|
||||
// --- lookups (providers / policy types / adjusters) -----------------------
|
||||
|
||||
listLookups() {
|
||||
return this.prisma.$transaction([
|
||||
this.prisma.insuranceProvider.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true, _count: { select: { policies: true } } },
|
||||
}),
|
||||
this.prisma.policyType.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
shortDescription: true,
|
||||
_count: { select: { policies: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.adjuster.findMany({ orderBy: { name: "asc" } }),
|
||||
]).then(([providers, types, adjusters]) => ({ providers, types, adjusters }));
|
||||
}
|
||||
|
||||
createProvider(dto: ProviderDto) {
|
||||
return this.prisma.insuranceProvider.create({ data: dto });
|
||||
}
|
||||
updateProvider(id: string, dto: UpdateProviderDto) {
|
||||
return this.prisma.insuranceProvider.update({ where: { id }, data: dto });
|
||||
}
|
||||
removeProvider(id: string) {
|
||||
return this.prisma.insuranceProvider.delete({ where: { id } });
|
||||
}
|
||||
|
||||
createPolicyType(dto: PolicyTypeDto) {
|
||||
return this.prisma.policyType.create({ data: dto });
|
||||
}
|
||||
updatePolicyType(id: string, dto: UpdatePolicyTypeDto) {
|
||||
return this.prisma.policyType.update({ where: { id }, data: dto });
|
||||
}
|
||||
removePolicyType(id: string) {
|
||||
return this.prisma.policyType.delete({ where: { id } });
|
||||
}
|
||||
|
||||
createAdjuster(dto: AdjusterDto) {
|
||||
return this.prisma.adjuster.create({ data: dto });
|
||||
}
|
||||
updateAdjuster(id: string, dto: UpdateAdjusterDto) {
|
||||
return this.prisma.adjuster.update({ where: { id }, data: dto });
|
||||
}
|
||||
removeAdjuster(id: string) {
|
||||
return this.prisma.adjuster.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MinLength,
|
||||
} from "class-validator";
|
||||
import { Currency } from "@jorgecuadros/database";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
/** Editable policy-header fields. coveragesJson (freeform legacy blob) is not
|
||||
* exposed for editing. Dates arrive as ISO strings and are coerced by the
|
||||
* service. `total` is legacy-dead data — the UI uses netPremium. */
|
||||
export class CreatePolicyDto {
|
||||
@IsString() @MinLength(1) policyNumber!: string;
|
||||
@IsString() @MinLength(1) customerId!: string;
|
||||
|
||||
@IsOptional() @IsString() policyTypeId?: string;
|
||||
@IsOptional() @IsString() insuranceProviderId?: string;
|
||||
@IsOptional() @IsString() agentName?: string;
|
||||
@IsOptional() @IsString() policyDate?: string;
|
||||
@IsOptional() @IsString() policyFrom?: string;
|
||||
@IsOptional() @IsString() policyTo?: string;
|
||||
@IsOptional() @IsInt() coveragePeriodDays?: number;
|
||||
@IsOptional() @IsNumber() netPremium?: number;
|
||||
@IsOptional() @IsNumber() policyFee?: number;
|
||||
@IsOptional() @IsNumber() brokerFee?: number;
|
||||
@IsOptional() @IsNumber() commission?: number;
|
||||
@IsOptional() @IsNumber() total?: number;
|
||||
@IsOptional() @IsEnum(Currency) currency?: Currency;
|
||||
@IsOptional() @IsString() observations?: string;
|
||||
@IsOptional() @IsString() notes?: string;
|
||||
@IsOptional() @IsBoolean() endorsement?: boolean;
|
||||
@IsOptional() @IsBoolean() liquidated?: boolean;
|
||||
@IsOptional() @IsString() liquidationNumber?: string;
|
||||
@IsOptional() @IsString() liquidationDate?: string;
|
||||
}
|
||||
|
||||
/** All header fields optional (customerId is not re-assignable on update). */
|
||||
export class UpdatePolicyDto {
|
||||
@IsOptional() @IsString() @MinLength(1) policyNumber?: string;
|
||||
@IsOptional() @IsString() policyTypeId?: string;
|
||||
@IsOptional() @IsString() insuranceProviderId?: string;
|
||||
@IsOptional() @IsString() agentName?: string;
|
||||
@IsOptional() @IsString() policyDate?: string;
|
||||
@IsOptional() @IsString() policyFrom?: string;
|
||||
@IsOptional() @IsString() policyTo?: string;
|
||||
@IsOptional() @IsInt() coveragePeriodDays?: number;
|
||||
@IsOptional() @IsNumber() netPremium?: number;
|
||||
@IsOptional() @IsNumber() policyFee?: number;
|
||||
@IsOptional() @IsNumber() brokerFee?: number;
|
||||
@IsOptional() @IsNumber() commission?: number;
|
||||
@IsOptional() @IsNumber() total?: number;
|
||||
@IsOptional() @IsEnum(Currency) currency?: Currency;
|
||||
@IsOptional() @IsString() observations?: string;
|
||||
@IsOptional() @IsString() notes?: string;
|
||||
@IsOptional() @IsBoolean() endorsement?: boolean;
|
||||
@IsOptional() @IsBoolean() liquidated?: boolean;
|
||||
@IsOptional() @IsString() liquidationNumber?: string;
|
||||
@IsOptional() @IsString() liquidationDate?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user