import { Body, Controller, Get, Param, 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 { BankCleared, BankDirection, BankService, BankSort, } from "./bank.service"; import { CreateBankMovementDto } from "./bank-movement.dto"; const DIRECTIONS: BankDirection[] = ["income", "expense", "void"]; const CLEARED: BankCleared[] = ["cleared", "pending"]; const SORTS: BankSort[] = [ "date_desc", "date_asc", "amount_desc", "amount_asc", "reference", ]; 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("bank") export class BankController { constructor( private readonly bank: BankService, private readonly audit: AuditService, ) {} private actingId(req: Request): string { return (req.user as { id: string }).id; } @Get("stats") stats() { return this.bank.stats(); } @Get("facets") facets() { return this.bank.facets(); } /** Year and month rollups with a running net-movement figure. */ @Get("summary") summary(@Query("year") year?: string) { const y = Number(year); return this.bank.summary( Number.isInteger(y) && y >= 1900 && y <= 2999 ? y : undefined, ); } /** The register browser. */ @Get() list( @Query("query") query?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("direction") direction?: string, @Query("cleared") cleared?: string, @Query("from") from?: string, @Query("to") to?: string, @Query("sort") sort?: string, ) { return this.bank.list({ query, page: Math.max(1, Number(page) || 1), pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)), direction: one(DIRECTIONS, direction), cleared: one(CLEARED, cleared), from: parseDate(from), to: parseDate(to, true), sort: one(SORTS, sort) ?? "date_desc", }); } // --- writes --------------------------------------------------------------- @Post() @RequireAbility("bank:create") async create(@Body() dto: CreateBankMovementDto, @Req() req: Request) { const row = await this.bank.createMovement(dto); void this.audit.log(this.actingId(req), "bank.create", { bankTransactionId: row.id, amount: dto.amount, }); return row; } @Post(":id/void") @RequireAbility("bank:void") async void(@Param("id") id: string, @Req() req: Request) { const row = await this.bank.voidMovement(id, this.actingId(req)); void this.audit.log(this.actingId(req), "bank.void", { bankTransactionId: id }); return row; } }