mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Param,
|
|
ParseUUIDPipe,
|
|
Post,
|
|
Query,
|
|
Res,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from "@nestjs/common";
|
|
import { FileInterceptor } from "@nestjs/platform-express";
|
|
import {
|
|
ApiBearerAuth,
|
|
ApiConsumes,
|
|
ApiOperation,
|
|
ApiTags,
|
|
} from "@nestjs/swagger";
|
|
import type { Response } from "express";
|
|
|
|
import { CurrentUser } from "@edr/api-common";
|
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
|
|
|
import { BookingStaff } from "../../common/booking-guards";
|
|
import { resolveAuthUserId } from "../../common/resolve-auth-user-id";
|
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
|
import { actorLabel } from "../warehouses/current-actor.util";
|
|
import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service";
|
|
import { BillingService } from "./billing.service";
|
|
import { FilterInvoiceDto } from "./dto/filter-invoice.dto";
|
|
|
|
@ApiTags("billing")
|
|
@Controller("billing")
|
|
// Class gate lists every key its routes use: Nest runs class AND method
|
|
// guards, so a key missing here would deny before the route's own key runs.
|
|
@BookingStaff([
|
|
FREIGHT_PERMS.invoices.view,
|
|
FREIGHT_PERMS.invoices.export,
|
|
FREIGHT_PERMS.invoices.confirmOffline,
|
|
])
|
|
@ApiBearerAuth()
|
|
export class BillingController {
|
|
constructor(
|
|
private readonly billingService: BillingService,
|
|
private readonly userTradeAccessService: UserTradeAccessService,
|
|
) {}
|
|
|
|
@Get("invoices")
|
|
@ApiOperation({
|
|
summary: "List invoices (paginated, filterable by company/status/search)",
|
|
})
|
|
async findAll(
|
|
@Query() query: FilterInvoiceDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
// Per-user trade-direction scope, applied via each invoice's source booking.
|
|
const allowed =
|
|
await this.userTradeAccessService.resolveAllowedDirections(user);
|
|
return this.billingService.findAllPaginated({
|
|
...query,
|
|
tradeDirections: allowed ?? undefined,
|
|
});
|
|
}
|
|
|
|
@Get("invoices/:id")
|
|
@ApiOperation({ summary: "Get an invoice with its line items" })
|
|
findById(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.billingService.findById(id);
|
|
}
|
|
|
|
@Get("offline-usd")
|
|
@ApiOperation({
|
|
summary:
|
|
"Finance worklist: USD invoices settled offline by bank transfer, with booking pay-window context",
|
|
})
|
|
findOfflineUsd(@Query() query: FilterInvoiceDto) {
|
|
return this.billingService.findOfflineUsdPaginated(query);
|
|
}
|
|
|
|
@Post("invoices/:id/confirm-offline")
|
|
@BookingStaff(FREIGHT_PERMS.invoices.confirmOffline)
|
|
@UseInterceptors(FileInterceptor("file"))
|
|
@ApiConsumes("multipart/form-data")
|
|
@ApiOperation({
|
|
summary:
|
|
"Finance confirms a USD invoice paid by bank transfer — slip file required, settles the full balance",
|
|
})
|
|
confirmOffline(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@UploadedFile() file: Express.Multer.File | undefined,
|
|
@Body("reference") reference: string | undefined,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.billingService.confirmOfflinePayment(id, file, {
|
|
reference: reference?.trim() || null,
|
|
userId: resolveAuthUserId(user),
|
|
userName: actorLabel(user) ?? null,
|
|
});
|
|
}
|
|
|
|
@Get("invoices/:id/document")
|
|
@BookingStaff(FREIGHT_PERMS.invoices.export)
|
|
@ApiOperation({ summary: "Download the sealed invoice PDF" })
|
|
async document(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) {
|
|
const { filename, buffer } = await this.billingService.document(id);
|
|
sendPdf(res, filename, buffer);
|
|
}
|
|
|
|
@Get("invoices/:id/receipt")
|
|
@BookingStaff(FREIGHT_PERMS.invoices.export)
|
|
@ApiOperation({ summary: "Download the sealed payment receipt PDF" })
|
|
async receipt(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) {
|
|
const { filename, buffer } = await this.billingService.receipt(id);
|
|
sendPdf(res, filename, buffer);
|
|
}
|
|
}
|
|
|
|
/** Stream a generated PDF as a file download. */
|
|
export function sendPdf(res: Response, filename: string, buffer: Buffer): void {
|
|
res.setHeader("Content-Type", "application/pdf");
|
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
res.setHeader("Content-Length", buffer.length);
|
|
res.send(buffer);
|
|
}
|