import { Body, Controller, Get, Param, Post, Query, Req, UseGuards, } from "@nestjs/common"; import { TransactionDomain } from "@jorgecuadros/database"; 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 { BalanceFilter, BalanceSort, BillingService, LedgerCurrency, LedgerDirection, MovementSort, } from "./billing.service"; import { CreateMovementDto } from "./movement.dto"; const DOMAINS: TransactionDomain[] = ["UTILITY", "INSURANCE", "TRUST"]; const CURRENCIES: LedgerCurrency[] = ["MXN", "USD"]; const DIRECTIONS: LedgerDirection[] = ["charge", "credit"]; const BALANCES: BalanceFilter[] = ["all", "owing", "credit", "settled"]; const MOVEMENT_SORTS: MovementSort[] = [ "date_desc", "date_asc", "amount_desc", "amount_asc", "customer", ]; const BALANCE_SORTS: BalanceSort[] = [ "owing_desc", "credit_desc", "recent", "customer", ]; function one(allowed: T[], value: string | undefined): T | undefined { return allowed.includes(value as T) ? (value as T) : undefined; } /** A `YYYY-MM-DD` bound; anything unparseable is treated as absent. */ function parseDate(v: string | undefined, endOfDay = false): Date | undefined { if (!v) return undefined; const d = new Date(endOfDay ? `${v}T23:59:59.999Z` : `${v}T00:00:00.000Z`); return Number.isNaN(d.getTime()) ? undefined : d; } @UseGuards(AuthenticatedGuard, AbilityGuard) @Controller("billing") export class BillingController { constructor( private readonly billing: BillingService, private readonly audit: AuditService, ) {} private actingId(req: Request): string { return (req.user as { id: string }).id; } @Get("stats") stats() { return this.billing.stats(); } @Get("facets") facets() { return this.billing.facets(); } /** Per-customer balances — the receivables worklist. */ @Get("balances") balances( @Query("query") query?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("currency") currency?: string, @Query("balance") balance?: string, @Query("domain") domain?: string, @Query("sort") sort?: string, ) { return this.billing.balances({ query, page: Math.max(1, Number(page) || 1), pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)), currency: one(CURRENCIES, currency) ?? "MXN", balance: one(BALANCES, balance) ?? "all", domain: one(DOMAINS, domain), sort: one(BALANCE_SORTS, sort) ?? "owing_desc", }); } /** One customer's full statement across both business lines. */ @Get("customers/:id") statement(@Param("id") id: string) { return this.billing.statement(id); } /** Cross-customer movement browser. */ @Get() movements( @Query("query") query?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("domain") domain?: string, @Query("currency") currency?: string, @Query("direction") direction?: string, @Query("typeId") typeId?: string, @Query("source") source?: string, @Query("customerId") customerId?: string, @Query("from") from?: string, @Query("to") to?: string, @Query("sort") sort?: string, ) { return this.billing.movements({ query, page: Math.max(1, Number(page) || 1), pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)), domain: one(DOMAINS, domain), currency: one(CURRENCIES, currency), direction: one(DIRECTIONS, direction), typeId: typeId || undefined, source: source || undefined, customerId: customerId || undefined, from: parseDate(from), to: parseDate(to, true), sort: one(MOVEMENT_SORTS, sort) ?? "date_desc", }); } // --- writes --------------------------------------------------------------- @Post() @RequireAbility("ledger:create") async create(@Body() dto: CreateMovementDto, @Req() req: Request) { const tx = await this.billing.createMovement(dto); void this.audit.log(this.actingId(req), "ledger.create", { transactionId: tx.id, customerId: dto.customerId, amount: dto.amount, currency: tx.currency, }); return tx; } @Post(":id/void") @RequireAbility("ledger:void") async void(@Param("id") id: string, @Req() req: Request) { const tx = await this.billing.voidMovement(id, this.actingId(req)); void this.audit.log(this.actingId(req), "ledger.void", { transactionId: id }); return tx; } }