diff --git a/apps/edr-freight-api/src/modules/billing/billing.controller.ts b/apps/edr-freight-api/src/modules/billing/billing.controller.ts index 7da5e2d66..b9f0a74c0 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.controller.ts @@ -1,21 +1,31 @@ -import { Controller, Get, Param, ParseUUIDPipe, Res } from "@nestjs/common"; +import { + Controller, + Get, + Param, + ParseUUIDPipe, + Query, + Res, +} from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import type { Response } from "express"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingView } from "../../common/booking-guards"; import { BillingService } from "./billing.service"; +import { FilterInvoiceDto } from "./dto/filter-invoice.dto"; @ApiTags("billing") @Controller("billing") -@FreightAdmin() +@BookingView() @ApiBearerAuth() export class BillingController { - constructor(private readonly billingService: BillingService) { } + constructor(private readonly billingService: BillingService) {} @Get("invoices") - @ApiOperation({ summary: "List all invoices" }) - findAll() { - return this.billingService.findAll(); + @ApiOperation({ + summary: "List invoices (paginated, filterable by company/status/search)", + }) + findAll(@Query() query: FilterInvoiceDto) { + return this.billingService.findAllPaginated(query); } @Get("invoices/:id") diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts index 82a47fa29..536129122 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -125,7 +125,7 @@ export class BillingService { private readonly payment: PaymentService, private readonly companies: CompaniesService, private readonly invoiceDocuments: InvoiceDocumentService, - ) { } + ) {} // ── Reads ────────────────────────────────────────────────────────────────── @@ -134,9 +134,56 @@ export class BillingService { return this.invoices.findAll({ order: { issuedAt: "DESC" } }); } + /** + * Paginated invoice list for the backoffice — optionally narrowed to a + * company (customer detail "Invoices" tab) and/or status/search (global + * invoices page). + */ + async findAllPaginated( + filter: { + companyId?: string; + status?: Freight.InvoiceStatus; + search?: string; + page?: number; + pageSize?: number; + } = {}, + ): Promise<{ items: Invoice[]; total: number }> { + const page = filter.page && filter.page > 0 ? filter.page : 1; + const pageSize = + filter.pageSize && filter.pageSize > 0 ? filter.pageSize : 20; + + const qb = this.dataSource + .getRepository(Invoice) + .createQueryBuilder("invoice") + .leftJoinAndSelect("invoice.company", "company") + .orderBy("invoice.issuedAt", "DESC") + .skip((page - 1) * pageSize) + .take(pageSize); + + if (filter.companyId) { + qb.andWhere("invoice.companyId = :companyId", { + companyId: filter.companyId, + }); + } + if (filter.status) { + qb.andWhere("invoice.status = :status", { status: filter.status }); + } + if (filter.search) { + qb.andWhere( + "(invoice.invoiceNumber ILIKE :search OR invoice.sourceId ILIKE :search)", + { search: `%${filter.search}%` }, + ); + } + + const [items, total] = await qb.getManyAndCount(); + return { items, total }; + } + /** Invoice header plus its line items. */ async findById(id: string): Promise { - const invoice = await this.invoices.findById(id); + const invoice = await this.invoices.findById(id, { + relations: { company: true, companyProfile: true }, + }); if (!invoice) throw new NotFoundException(`Invoice ${id} not found`); const lines = await this.invoiceLines.findAll({ where: { invoiceId: id }, @@ -375,7 +422,7 @@ export class BillingService { input.dueAt ?? new Date( Date.now() + - (input.dueInDays ?? DEFAULT_DUE_DAYS) * 24 * 60 * 60 * 1000, + (input.dueInDays ?? DEFAULT_DUE_DAYS) * 24 * 60 * 60 * 1000, ); const invoiceNumber = await this.nextInvoiceNumber(mg); diff --git a/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts new file mode 100644 index 000000000..e8942d586 --- /dev/null +++ b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts @@ -0,0 +1,42 @@ +import { Freight } from "@edr/types"; +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Transform } from "class-transformer"; +import { + IsIn, + IsInt, + IsOptional, + IsString, + IsUUID, + Min, +} from "class-validator"; + +export class FilterInvoiceDto { + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Transform(({ value }: { value: unknown }) => parseInt(String(value), 10)) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 20 }) + @IsOptional() + @Transform(({ value }: { value: unknown }) => parseInt(String(value), 10)) + @IsInt() + @Min(1) + pageSize?: number = 20; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID() + companyId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ enum: Freight.InvoiceStatus }) + @IsOptional() + @IsIn(Object.values(Freight.InvoiceStatus)) + status?: Freight.InvoiceStatus; +}