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 { CustomersService } from "./customers.service"; import { NumidService } from "./numid.service"; import { CreateCustomerDto } from "./create-customer.dto"; import { UpdateCustomerDto } from "./update-customer.dto"; @UseGuards(AuthenticatedGuard, AbilityGuard) @Controller("customers") export class CustomersController { constructor( private readonly customers: CustomersService, private readonly numids: NumidService, private readonly audit: AuditService, ) {} private actingId(req: Request): string { return (req.user as { id: string }).id; } @Get("stats") stats() { return this.customers.stats(); } /** Reusable portal ids, lowest first. Declared above `:id` so the literal * path is not swallowed by the wildcard route. */ @Get("numid/candidates") async numidCandidates() { return { candidates: await this.numids.emptyCandidates() }; } @Get() list( @Query("query") query?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("line") line?: "utility" | "insurance" | "both", @Query("includeArchived") includeArchived?: string, ) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 25)); return this.customers.list({ query, page: p, pageSize: ps, line, includeArchived: includeArchived === "true", }); } @Get(":id") detail(@Param("id") id: string) { return this.customers.detail(id); } @Post() @RequireAbility("customer:create") async create(@Body() dto: CreateCustomerDto, @Req() req: Request) { const c = await this.customers.create(dto); void this.audit.log(this.actingId(req), "customer.create", { customerId: c.id, name: c.name }); return c; } @Patch(":id") @RequireAbility("customer:update") async update( @Param("id") id: string, @Body() dto: UpdateCustomerDto, @Req() req: Request, ) { const c = await this.customers.update(id, dto); void this.audit.log(this.actingId(req), "customer.update", { customerId: id }); return c; } @Delete(":id") @RequireAbility("customer:delete") async archive(@Param("id") id: string, @Req() req: Request) { const c = await this.customers.archive(id); void this.audit.log(this.actingId(req), "customer.archive", { customerId: id }); return c; } @Post(":id/restore") @RequireAbility("customer:delete") async restore(@Param("id") id: string, @Req() req: Request) { const c = await this.customers.restore(id); void this.audit.log(this.actingId(req), "customer.restore", { customerId: id }); return c; } /** * Give this customer a portal NUMid so they can log in to * my.jorgecuadros.com. Idempotent — a customer who already has one gets it * back rather than a second identity. */ @Post(":id/portal-access") @RequireAbility("customer:portal-access") async portalAccess(@Param("id") id: string, @Req() req: Request) { const allocation = await this.numids.allocate(id); if (allocation.origin !== "existing") { // Logged with the origin and the previous holder: a recycled id is the one // case where reading this record later has to answer "whose number was // this before, and was it taken or minted". void this.audit.log(this.actingId(req), "customer.portal-access", { customerId: id, numid: allocation.numid, origin: allocation.origin, previousCustomerId: allocation.previousCustomerId, }); } return allocation; } }