From 588e9dd733c8f1a4bf538b08ad2d12637ada7d86 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 7 Jul 2026 15:29:30 +0200 Subject: [PATCH] Proformas preview --- modules/customer-invoices/package.json | 1 + .../proformas/di/proforma-use-cases.di.ts | 30 ++++- .../company-report-profile-not-found.error.ts | 15 +++ .../api/application/proformas/errors/index.ts | 1 + .../src/api/application/proformas/index.ts | 1 + .../models/company-report-profile.model.ts | 30 +++++ .../api/application/proformas/models/index.ts | 2 + .../models/report-localization.model.ts | 5 + .../services/company-report-profile-finder.ts | 11 ++ .../application/proformas/services/index.ts | 2 +- .../proforma-document-metadata-factory.ts | 47 ------- .../proforma-document-properties-factory.ts | 4 +- .../snapshot-builders/report/index.ts | 1 - .../proforma-items-report-snapshot-builder.ts | 27 ++-- .../proforma-report-snapshot-builder.ts | 29 +++- .../proforma-report-snapshot.interface.ts | 13 +- .../proforma-tax-report-snapshot-builder.ts | 27 ++-- .../application/proformas/use-cases/index.ts | 3 +- .../preview-proforma-report.use-case.ts | 124 ++++++++++++++++++ .../use-cases/report-proforma-pdf.use-case.ts | 124 ++++++++++++++++++ .../use-cases/report-proforma.use-case.ts | 80 ----------- .../use-cases/report-proforma2/index.ts | 2 - .../report-proforma.use-case.ts | 67 ---------- .../report-proforma2/reporter/index.ts | 0 .../reporter/proforma.report.html.ts | 32 ----- .../reporter/proforma.report.pdf.ts | 76 ----------- .../adapters/company-report-profile-finder.ts | 51 +++++++ .../proformas/di/proforma-documents.di.ts | 29 ++-- .../proformas/di/proforma-public-services.ts | 1 + .../proformas/di/proformas.di.ts | 37 ++++-- .../proformas/documents/pipelines/index.ts | 3 +- .../proforma-document-pipeline-factory.ts | 71 ---------- .../proforma-html-preview-pipeline-factory.ts | 40 ++++++ .../proforma-pdf-document-pipeline-factory.ts | 49 +++++++ .../documents/pre-processors/index.ts | 2 +- .../proforma-document-cache-pre-processor.ts | 71 ---------- .../proforma-pdf-cache-pre-processor.ts | 34 +++++ .../documents/renderers/fastreport/index.ts | 3 +- .../fastreport/proforma-document-renderer.ts | 72 ---------- .../proforma-html-preview-renderer.ts | 50 +++++++ .../proforma-pdf-document-renderer.ts | 50 +++++++ ...-html-preview-document-metadata-factory.ts | 19 +++ .../proforma-pdf-document-metadata-factory.ts | 48 +++++++ .../proformas/documents/side-effects/index.ts | 2 +- .../persist-proforma-document-side-effect.ts | 27 ---- .../persist-proforma-pdf-side-effect.ts | 18 +++ .../proformas/express/controllers/index.ts | 3 +- .../preview-proforma-report.controller.ts | 41 ++++++ .../report-proforma-pdf.controller.ts | 41 ++++++ .../controllers/report-proforma.controller.ts | 55 -------- .../express/proformas-api-error-mapper.ts | 12 ++ .../proformas/express/proformas.routes.ts | 28 ++-- .../src/common/dto/request/proformas/index.ts | 3 +- ...eview-proforma-report-by-id.request.dto.ts | 9 ++ .../report-proforma-by-id.request.dto.ts | 21 --- .../report-proforma-pdf-by-id.request.dto.ts | 9 ++ .../common/dto/response/proformas/index.ts | 1 - .../report-proforma-by-id.response.dto.ts | 1 - .../use-proforma-grid-columns.tsx | 43 +++++- .../list/ui/pages/list-proformas-page.tsx | 2 + .../shared/api/report-proforma-by-id.api.ts | 23 +--- 61 files changed, 1004 insertions(+), 719 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/proformas/errors/company-report-profile-not-found.error.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/errors/index.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/models/company-report-profile.model.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/models/report-localization.model.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/services/company-report-profile-finder.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/index.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/adapters/company-report-profile-finder.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-html-preview-pipeline-factory.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-pdf-document-pipeline-factory.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-pdf-cache-pre-processor.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-html-preview-renderer.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-pdf-document-renderer.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-html-preview-document-metadata-factory.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-pdf-document-metadata-factory.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-pdf-side-effect.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/preview-proforma-report.controller.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma-pdf.controller.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma.controller.ts create mode 100644 modules/customer-invoices/src/common/dto/request/proformas/preview-proforma-report-by-id.request.dto.ts delete mode 100644 modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts create mode 100644 modules/customer-invoices/src/common/dto/request/proformas/report-proforma-pdf-by-id.request.dto.ts delete mode 100644 modules/customer-invoices/src/common/dto/response/proformas/report-proforma-by-id.response.dto.ts diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 80033e55..6350b60e 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -39,6 +39,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@erp/companies": "workspace:*", "@erp/core": "workspace:*", "@erp/catalogs": "workspace:*", "@erp/customers": "workspace:*", diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts index 909ce218..48584241 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -4,6 +4,7 @@ import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers"; import type { IProformaRepository } from "../repositories"; import type { + ICompanyReportProfileFinder, IProformaCreator, IProformaFinder, IProformaFullReadModelAssembler, @@ -24,7 +25,8 @@ import { GetProformaByIdUseCase, IssueProformaUseCase, ListProformasUseCase, - ReportProformaUseCase, + PreviewProformaReportUseCase, + ReportProformaPdfUseCase, UpdateProformaByIdUseCase, } from "../use-cases"; @@ -54,16 +56,38 @@ export function buildListProformasUseCase(deps: { ); } -export function buildReportProformaUseCase(deps: { +export function buildReportProformaPdfUseCase(deps: { finder: IProformaFinder; + companyReportProfileFinder: ICompanyReportProfileFinder; fullReadModelAssembler: IProformaFullReadModelAssembler; fullSnapshotBuilder: IProformaFullSnapshotBuilder; reportSnapshotBuilder: IProformaReportSnapshotBuilder; documentService: ProformaDocumentGeneratorService; transactionManager: ITransactionManager; }) { - return new ReportProformaUseCase({ + return new ReportProformaPdfUseCase({ finder: deps.finder, + companyReportProfileFinder: deps.companyReportProfileFinder, + fullReadModelAssembler: deps.fullReadModelAssembler, + fullSnapshotBuilder: deps.fullSnapshotBuilder, + reportSnapshotBuilder: deps.reportSnapshotBuilder, + documentService: deps.documentService, + transactionManager: deps.transactionManager, + }); +} + +export function buildPreviewProformaReportUseCase(deps: { + finder: IProformaFinder; + companyReportProfileFinder: ICompanyReportProfileFinder; + fullReadModelAssembler: IProformaFullReadModelAssembler; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + reportSnapshotBuilder: IProformaReportSnapshotBuilder; + documentService: ProformaDocumentGeneratorService; + transactionManager: ITransactionManager; +}) { + return new PreviewProformaReportUseCase({ + finder: deps.finder, + companyReportProfileFinder: deps.companyReportProfileFinder, fullReadModelAssembler: deps.fullReadModelAssembler, fullSnapshotBuilder: deps.fullSnapshotBuilder, reportSnapshotBuilder: deps.reportSnapshotBuilder, diff --git a/modules/customer-invoices/src/api/application/proformas/errors/company-report-profile-not-found.error.ts b/modules/customer-invoices/src/api/application/proformas/errors/company-report-profile-not-found.error.ts new file mode 100644 index 00000000..3816d96a --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/errors/company-report-profile-not-found.error.ts @@ -0,0 +1,15 @@ +export class CompanyReportProfileNotFoundError extends Error { + public readonly code = "COMPANY_REPORT_PROFILE_NOT_FOUND"; + + public constructor(companyId: string) { + super( + `Cannot generate the proforma report because the company document profile is missing for company ${companyId}.` + ); + this.name = "CompanyReportProfileNotFoundError"; + } +} + +export const isCompanyReportProfileNotFoundError = ( + error: unknown +): error is CompanyReportProfileNotFoundError => + error instanceof CompanyReportProfileNotFoundError; diff --git a/modules/customer-invoices/src/api/application/proformas/errors/index.ts b/modules/customer-invoices/src/api/application/proformas/errors/index.ts new file mode 100644 index 00000000..315fe5a9 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/errors/index.ts @@ -0,0 +1 @@ +export * from "./company-report-profile-not-found.error"; diff --git a/modules/customer-invoices/src/api/application/proformas/index.ts b/modules/customer-invoices/src/api/application/proformas/index.ts index b68ea0dc..1e581472 100644 --- a/modules/customer-invoices/src/api/application/proformas/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/index.ts @@ -1,4 +1,5 @@ export * from "./di"; +export * from "./errors"; export * from "./mappers"; export * from "./models"; export * from "./repositories"; diff --git a/modules/customer-invoices/src/api/application/proformas/models/company-report-profile.model.ts b/modules/customer-invoices/src/api/application/proformas/models/company-report-profile.model.ts new file mode 100644 index 00000000..68ebe00c --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/models/company-report-profile.model.ts @@ -0,0 +1,30 @@ +import type { Maybe } from "@repo/rdx-utils"; + +export type CompanyReportLogo = { + url: string; +}; + +export type CompanyReportAddress = { + street: string; + street2: Maybe; + postalCode: Maybe; + city: Maybe; + province: Maybe; + country: Maybe; +}; + +export type CompanyReportProfile = { + companyId: string; + slug: string; + legalName: string; + tradeName: Maybe; + tin: Maybe; + email: Maybe; + phone: Maybe; + website: Maybe; + locale: Maybe; + currencyCode: Maybe; + logo: Maybe; + address: Maybe; + version: Maybe; +}; diff --git a/modules/customer-invoices/src/api/application/proformas/models/index.ts b/modules/customer-invoices/src/api/application/proformas/models/index.ts index cc215b9b..6f2e629a 100644 --- a/modules/customer-invoices/src/api/application/proformas/models/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/models/index.ts @@ -1,5 +1,7 @@ +export * from "./company-report-profile.model"; export * from "./proforma-create-input.model"; export * from "./proforma-full-read.model"; export * from "./proforma-item-tax-codes-input.model"; export * from "./proforma-summary"; export * from "./proforma-update-input.model"; +export * from "./report-localization.model"; diff --git a/modules/customer-invoices/src/api/application/proformas/models/report-localization.model.ts b/modules/customer-invoices/src/api/application/proformas/models/report-localization.model.ts new file mode 100644 index 00000000..570c0231 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/models/report-localization.model.ts @@ -0,0 +1,5 @@ +export type ReportLocalization = { + languageCode: string; + locale: string; + currencyCode: string; +}; diff --git a/modules/customer-invoices/src/api/application/proformas/services/company-report-profile-finder.ts b/modules/customer-invoices/src/api/application/proformas/services/company-report-profile-finder.ts new file mode 100644 index 00000000..03af3939 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/company-report-profile-finder.ts @@ -0,0 +1,11 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { CompanyReportProfile } from "../models"; + +export interface ICompanyReportProfileFinder { + findByCompanyId( + companyId: UniqueID, + transaction?: unknown + ): Promise, Error>>; +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts index a32f9629..c8817337 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -1,8 +1,8 @@ export * from "./assemblers"; export * from "./catalog-resolver"; +export * from "./company-report-profile-finder"; export * from "./proforma-creator"; export * from "./proforma-document-generator.interface"; -export * from "./proforma-document-metadata-factory"; export * from "./proforma-document-properties-factory"; export * from "./proforma-finder"; export * from "./proforma-issuer"; diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts deleted file mode 100644 index f60be3f7..00000000 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; - -import type { ProformaReportSnapshot } from "../snapshot-builders"; - -/** - * Construye los metadatos del documento PDF de una factura emitida. - * - * - Application-level - * - Determinista - * - Sin IO - */ -export class ProformaDocumentMetadataFactory - implements IDocumentMetadataFactory -{ - build(snapshot: ProformaReportSnapshot): IDocumentMetadata { - if (!snapshot.id) { - throw new Error("ProformaReportSnapshot.id is required"); - } - - if (!snapshot.company_id) { - throw new Error("ProformaReportSnapshot.companyId is required"); - } - - return { - documentType: "proforma", - documentId: snapshot.id, - companyId: snapshot.company_id, - companySlug: snapshot.company_slug, - format: "PDF", - languageCode: snapshot.language_code ?? "es", - filename: this.buildFilename(snapshot), - storageKey: this.buildCacheKey(snapshot), - }; - } - - private buildFilename(snapshot: ProformaReportSnapshot): string { - // Ejemplo: factura-F2024-000123-FULANITO.pdf - return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`; - } - - private buildCacheKey(snapshot: ProformaReportSnapshot): string { - // Versionado explícito para invalidaciones futuras - return ["proforma", snapshot.company_id, snapshot.series, snapshot.invoice_number, "v1"].join( - ":" - ); - } -} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts index 5843223f..e803945c 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts @@ -14,9 +14,9 @@ export class ProformaDocumentPropertiesFactory { build(snapshot: ProformaReportSnapshot): IDocumentProperties { return { - title: snapshot.reference, + title: snapshot.reference ?? `Proforma ${snapshot.series}${snapshot.invoice_number}`, subject: "proforma", - author: snapshot.company_slug, + author: snapshot.company.legalName, creator: "FactuGES ERP", }; } diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts index fc118ac6..d1b38528 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts @@ -2,6 +2,5 @@ export * from "./proforma-items-report-snapshot-builder"; export * from "./proforma-report-item-snapshot.interface"; export * from "./proforma-report-snapshot.interface"; export * from "./proforma-report-snapshot-builder"; -export * from "./proforma-report-snapshot-builder"; export * from "./proforma-report-tax-snapshot.interface"; export * from "./proforma-tax-report-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts index ab927071..4f21c08c 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts @@ -2,18 +2,21 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/cor import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; import type { ProformaFullSnapshot } from "../full"; - import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; +type ProformaItemReportSnapshotBuilderParams = ISnapshotBuilderParams & { + locale: string; +}; + export interface IProformaItemReportSnapshotBuilder extends ISnapshotBuilder {} export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder { toOutput( items: ProformaFullSnapshot["items"], - params?: ISnapshotBuilderParams + params?: ProformaItemReportSnapshotBuilderParams ): ProformaReportItemSnapshot[] { - const locale = params?.locale as string; + const locale = params?.locale ?? "es-ES"; const moneyOptions = { hideZeros: true, @@ -21,14 +24,18 @@ export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSna }; return items.map((item) => ({ - description: item.description, - quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }), - unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions), + description: item.description ?? "", + quantity: item.quantity + ? QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }) + : "", + unit_amount: item.unit_amount ? MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions) : "", subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions), - discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, { - minimumFractionDigits: 0, - }), - discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions), + discount_percentage: item.item_discount_percentage + ? PercentageDTOHelper.format(item.item_discount_percentage, locale, { + minimumFractionDigits: 0, + }) + : "", + discount_amount: MoneyDTOHelper.format(item.item_discount_amount, locale, moneyOptions), taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions), total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions), diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts index b906e9ca..087c19a7 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts @@ -2,12 +2,17 @@ import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; import { DateHelper } from "@repo/rdx-utils"; +import type { CompanyReportProfile, ReportLocalization } from "../../models"; import type { ProformaFullSnapshot } from "../full"; - import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface"; import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface"; +export type ProformaReportSnapshotBuilderParams = ISnapshotBuilderParams & { + company: CompanyReportProfile; + localization: ReportLocalization; +}; + export interface IProformaReportSnapshotBuilder extends ISnapshotBuilder {} @@ -25,9 +30,20 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui toOutput( snapshot: ProformaFullSnapshot, - params?: ISnapshotBuilderParams + params?: ProformaReportSnapshotBuilderParams ): ProformaReportSnapshot { - const locale = params?.locale as string; + const localization = params?.localization; + const company = params?.company; + + if (!localization) { + throw new Error("Report localization is required"); + } + + if (!company) { + throw new Error("Company report profile is required"); + } + + const { locale } = localization; const moneyOptions = { hideZeros: true, @@ -37,22 +53,23 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui return { id: snapshot.id, company_id: snapshot.company_id, - company_slug: "rodax", invoice_number: snapshot.invoice_number, series: snapshot.series, status: snapshot.status, reference: snapshot.reference, + localization, + company, language_code: snapshot.language_code, currency_code: snapshot.currency_code, invoice_date: DateHelper.format(snapshot.invoice_date, locale), - payment_method: snapshot.payment_method?.payment_description ?? "", + payment_method: snapshot.payment_method?.description ?? snapshot.payment_method?.name ?? "", notes: snapshot.notes, recipient: { - name: snapshot.recipient.name, + name: snapshot.recipient.name ?? "", tin: snapshot.recipient.tin, format_address: this.formatAddress(snapshot.recipient), }, diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts index a8d4a59c..bd467e12 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts @@ -1,25 +1,28 @@ +import type { CompanyReportProfile, ReportLocalization } from "../../models"; import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface"; export interface ProformaReportSnapshot { id: string; company_id: string; - company_slug: string; invoice_number: string; - series: string; + series: string | null; status: string; - reference: string; + reference: string | null; + + localization: ReportLocalization; + company: CompanyReportProfile; language_code: string; currency_code: string; invoice_date: string; payment_method: string; - notes: string; + notes: string | null; recipient: { name: string; - tin: string; + tin: string | null; format_address: string; }; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts index 2b55ec3d..31faf557 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts @@ -1,7 +1,12 @@ import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; -import type { ProformaFullSnapshot, ProformaReportTaxSnapshot } from "../../application-models"; +import type { ProformaFullSnapshot } from "../full"; +import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface"; + +type ProformaTaxReportSnapshotBuilderParams = ISnapshotBuilderParams & { + locale: string; +}; export interface IProformaTaxReportSnapshotBuilder extends ISnapshotBuilder {} @@ -9,9 +14,9 @@ export interface IProformaTaxReportSnapshotBuilder export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder { toOutput( taxes: ProformaFullSnapshot["taxes"], - params?: ISnapshotBuilderParams + params?: ProformaTaxReportSnapshotBuilderParams ): ProformaReportTaxSnapshot[] { - const locale = params?.locale as string; + const locale = params?.locale ?? "es-ES"; const moneyOptions = { hideZeros: true, @@ -21,16 +26,20 @@ export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnaps return taxes.map((tax) => ({ taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions), - iva_code: tax.iva_code, - iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale), + iva_code: tax.iva_code ?? "", + iva_percentage: tax.iva_percentage + ? PercentageDTOHelper.format(tax.iva_percentage, locale) + : "", iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions), - rec_code: tax.rec_code, - rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale), + rec_code: tax.rec_code ?? "", + rec_percentage: tax.rec_percentage ? PercentageDTOHelper.format(tax.rec_percentage, locale) : "", rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions), - retention_code: tax.retention_code, - retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale), + retention_code: tax.retention_code ?? "", + retention_percentage: tax.retention_percentage + ? PercentageDTOHelper.format(tax.retention_percentage, locale) + : "", retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(tax.taxes_amount, locale, moneyOptions), diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts index e23936a7..d8330669 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts @@ -4,5 +4,6 @@ export * from "./create-proforma.use-case"; export * from "./get-proforma-by-id.use-case"; export * from "./issue-proforma.use-case"; export * from "./list-proformas.use-case"; -export * from "./report-proforma.use-case"; +export * from "./preview-proforma-report.use-case"; +export * from "./report-proforma-pdf.use-case"; export * from "./update-proforma-by-id.use-case"; diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.ts new file mode 100644 index 00000000..fff8a896 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.ts @@ -0,0 +1,124 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import { CompanyReportProfileNotFoundError } from "../errors"; +import type { + ICompanyReportProfileFinder, + IProformaFinder, + IProformaFullReadModelAssembler, + ProformaDocumentGeneratorService, +} from "../services"; +import type { ReportLocalization } from "../models"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; +import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report"; + +type PreviewProformaReportUseCaseInput = { + companyId: UniqueID; + proforma_id: string; +}; + +export class PreviewProformaReportUseCase { + public constructor( + private readonly deps: { + finder: IProformaFinder; + companyReportProfileFinder: ICompanyReportProfileFinder; + fullReadModelAssembler: IProformaFullReadModelAssembler; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + reportSnapshotBuilder: IProformaReportSnapshotBuilder; + documentService: ProformaDocumentGeneratorService; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: PreviewProformaReportUseCaseInput) { + const { proforma_id, companyId } = params; + + const idOrError = UniqueID.create(proforma_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const proformaId = idOrError.data; + + return this.deps.transactionManager.complete(async (transaction) => { + try { + const proformaResult = await this.deps.finder.findProformaById( + companyId, + proformaId, + transaction + ); + + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const readModelResult = await this.deps.fullReadModelAssembler.assemble({ + companyId, + proforma: proformaResult.data, + transaction, + }); + + if (readModelResult.isFailure) { + return Result.fail(readModelResult.error); + } + + const companyProfileResult = await this.deps.companyReportProfileFinder.findByCompanyId( + companyId, + transaction + ); + + if (companyProfileResult.isFailure) { + return Result.fail(companyProfileResult.error); + } + + if (companyProfileResult.data.isNone()) { + return Result.fail(new CompanyReportProfileNotFoundError(companyId.toString())); + } + + const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data); + const localization = this.resolveLocalization({ + languageCode: fullSnapshot.language_code, + currencyCode: fullSnapshot.currency_code, + companyLocale: companyProfileResult.data.unwrap().locale, + companyCurrencyCode: companyProfileResult.data.unwrap().currencyCode, + }); + + const reportSnapshot = this.deps.reportSnapshotBuilder.toOutput(fullSnapshot, { + localization, + company: companyProfileResult.data.unwrap(), + }); + + const documentResult = await this.deps.documentService.generate(reportSnapshot, { + companySlug: reportSnapshot.company.slug, + }); + + if (documentResult.isFailure) { + return Result.fail(documentResult.error); + } + + return Result.ok({ + payload: documentResult.data.payload.toString("utf-8"), + mimeType: "text/html; charset=utf-8" as const, + filename: documentResult.data.filename, + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } + + private resolveLocalization(params: { + languageCode: string; + currencyCode: string; + companyLocale: Maybe; + companyCurrencyCode: Maybe; + }): ReportLocalization { + return { + languageCode: params.languageCode || "es", + locale: params.companyLocale.getOrUndefined() ?? "es-ES", + currencyCode: params.currencyCode || params.companyCurrencyCode.getOrUndefined() || "EUR", + }; + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.ts new file mode 100644 index 00000000..75944099 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.ts @@ -0,0 +1,124 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import { CompanyReportProfileNotFoundError } from "../errors"; +import type { + ICompanyReportProfileFinder, + IProformaFinder, + IProformaFullReadModelAssembler, + ProformaDocumentGeneratorService, +} from "../services"; +import type { ReportLocalization } from "../models"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; +import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report"; + +type ReportProformaPdfUseCaseInput = { + companyId: UniqueID; + proforma_id: string; +}; + +export class ReportProformaPdfUseCase { + public constructor( + private readonly deps: { + finder: IProformaFinder; + companyReportProfileFinder: ICompanyReportProfileFinder; + fullReadModelAssembler: IProformaFullReadModelAssembler; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + reportSnapshotBuilder: IProformaReportSnapshotBuilder; + documentService: ProformaDocumentGeneratorService; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: ReportProformaPdfUseCaseInput) { + const { proforma_id, companyId } = params; + + const idOrError = UniqueID.create(proforma_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const proformaId = idOrError.data; + + return this.deps.transactionManager.complete(async (transaction) => { + try { + const proformaResult = await this.deps.finder.findProformaById( + companyId, + proformaId, + transaction + ); + + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const readModelResult = await this.deps.fullReadModelAssembler.assemble({ + companyId, + proforma: proformaResult.data, + transaction, + }); + + if (readModelResult.isFailure) { + return Result.fail(readModelResult.error); + } + + const companyProfileResult = await this.deps.companyReportProfileFinder.findByCompanyId( + companyId, + transaction + ); + + if (companyProfileResult.isFailure) { + return Result.fail(companyProfileResult.error); + } + + if (companyProfileResult.data.isNone()) { + return Result.fail(new CompanyReportProfileNotFoundError(companyId.toString())); + } + + const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data); + const localization = this.resolveLocalization({ + languageCode: fullSnapshot.language_code, + currencyCode: fullSnapshot.currency_code, + companyLocale: companyProfileResult.data.unwrap().locale, + companyCurrencyCode: companyProfileResult.data.unwrap().currencyCode, + }); + + const reportSnapshot = this.deps.reportSnapshotBuilder.toOutput(fullSnapshot, { + localization, + company: companyProfileResult.data.unwrap(), + }); + + const documentResult = await this.deps.documentService.generate(reportSnapshot, { + companySlug: reportSnapshot.company.slug, + }); + + if (documentResult.isFailure) { + return Result.fail(documentResult.error); + } + + return Result.ok({ + payload: documentResult.data.payload, + mimeType: "application/pdf" as const, + filename: documentResult.data.filename, + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } + + private resolveLocalization(params: { + languageCode: string; + currencyCode: string; + companyLocale: Maybe; + companyCurrencyCode: Maybe; + }): ReportLocalization { + return { + languageCode: params.languageCode || "es", + locale: params.companyLocale.getOrUndefined() ?? "es-ES", + currencyCode: params.currencyCode || params.companyCurrencyCode.getOrUndefined() || "EUR", + }; + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts deleted file mode 100644 index af070efe..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { type ITransactionManager, type RendererFormat, logger } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services"; -import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; -import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report"; - -type ReportProformaUseCaseInput = { - companyId: UniqueID; - proforma_id: string; - format: RendererFormat; -}; - -export class ReportProformaUseCase { - constructor( - private readonly finder: IProformaFinder, - private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder, - private readonly reportSnapshotBuilder: IProformaReportSnapshotBuilder, - private readonly documentGenerationService: ProformaDocumentGeneratorService, - private readonly transactionManager: ITransactionManager - ) {} - - public async execute(params: ReportProformaUseCaseInput) { - const { proforma_id, companyId } = params; - - const idOrError = UniqueID.create(proforma_id); - - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - const proformaId = idOrError.data; - - return this.transactionManager.complete(async (transaction) => { - try { - const proformaResult = await this.finder.findProformaById( - companyId, - proformaId, - transaction - ); - - if (proformaResult.isFailure) { - return Result.fail(proformaResult.error); - } - - const invoice = proformaResult.data; - - // Snapshot completo de la entidad - const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice); - - // Snapshot para informe a partir del anterior - const reportSnapshot = this.reportSnapshotBuilder.toOutput(fullSnapshot, { - languageCode: invoice.languageCode, - }); - - // Llamar al servicio y que se apañe - const documentResult = await this.documentGenerationService.generate(reportSnapshot, { - companySlug: "rodax", - }); - - if (documentResult.isFailure) { - return Result.fail(documentResult.error); - } - 11; - - // 5. Devolver artefacto firmado - return Result.ok({ - payload: documentResult.data.payload, - mimeType: "application/pdf", - filename: documentResult.data.filename, - }); - } catch (error: unknown) { - const err = error as Error; - logger.error(err.message, { label: "ReportProformaUseCase.execure" }); - return Result.fail(err); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts deleted file mode 100644 index 79d1f5a4..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./report-proforma.use-case"; -export * from "./reporter"; diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts deleted file mode 100644 index 32efd399..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; - -type ReportProformaUseCaseInput = { - companyId: UniqueID; - companySlug: string; - proforma_id: string; - format: "pdf" | "html"; -}; - -export class ReportProformaUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) {} - - public async execute(params: ReportProformaUseCaseInput) { - const { proforma_id, companySlug, companyId, format } = params; - - const idOrError = UniqueID.create(proforma_id); - - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - const proformaId = idOrError.data; - const reportPresenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "REPORT", - format, - }); - - return this.transactionManager.complete(async (transaction) => { - try { - const proformaOrError = await this.service.getProformaByIdInCompany( - companyId, - proformaId, - transaction - ); - if (proformaOrError.isFailure) { - return Result.fail(proformaOrError.error); - } - - const proforma = proformaOrError.data; - const reportData = await reportPresenter.toOutput(proforma, { companySlug }); - - if (format === "html") { - return Result.ok({ - data: String(reportData), - filename: undefined, - }); - } - - return Result.ok({ - data: reportData as Buffer, - filename: `proforma-${proforma.invoiceNumber}.pdf`, - }); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts deleted file mode 100644 index efcc8b39..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TemplatePresenter } from "@erp/core/api"; - -import type { Proforma } from "../../../../../domain"; -import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../snapshot-builders"; - -export class ProformaReportHTMLPresenter extends TemplatePresenter { - toOutput(proforma: Proforma, params: { companySlug: string }): string { - const { companySlug } = params; - const dtoPresenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "FULL", - }) as ProformaFullPresenter; - - const prePresenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "REPORT", - format: "JSON", - }) as ProformaReportPresenter; - - const invoiceDTO = dtoPresenter.toOutput(proforma); - const prettyDTO = prePresenter.toOutput(invoiceDTO); - - // Obtener y compilar la plantilla HTML - const template = this.templateResolver.compileTemplate( - "customer-invoices", - companySlug, - "proforma.hbs" - ); - - return template(prettyDTO); - } -} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts deleted file mode 100644 index 8b38e294..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Presenter } from "@erp/core/api"; -import puppeteer from "puppeteer"; - -import type { Proforma } from "../../../../../domain"; - -import type { ProformaReportHTMLPresenter } from "./proforma.report.html"; - -// https://plnkr.co/edit/lWk6Yd?preview -// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports - -export class ProformaReportPDFPresenter extends Presenter>> { - async toOutput( - proforma: Proforma, - params: { companySlug: string } - ): Promise> { - try { - const htmlPresenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "REPORT", - format: "HTML", - }) as ProformaReportHTMLPresenter; - - const htmlData = htmlPresenter.toOutput(proforma, params); - - // Generar el PDF con Puppeteer - const browser = await puppeteer.launch({ - headless: true, - args: [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--disable-extensions", - "--font-render-hinting=medium", - ], - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - }); - - const page = await browser.newPage(); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(60000); - - await page.setContent(htmlData, { - waitUntil: "networkidle2", - }); - - // Espera extra opcional si hay imágenes base64 muy grandes - await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); - - const reportPDF = await page.pdf({ - format: "A4", - margin: { - bottom: 0, - left: 0, - right: 0, - top: 0, - }, - landscape: false, - preferCSSPageSize: true, - omitBackground: false, - printBackground: true, - displayHeaderFooter: true, - headerTemplate: "
", - footerTemplate: - '
Página de
', - }); - - await browser.close(); - - return Buffer.from(reportPDF); - } catch (err: unknown) { - console.error(err); - throw err as Error; - } - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/adapters/company-report-profile-finder.ts b/modules/customer-invoices/src/api/infrastructure/proformas/adapters/company-report-profile-finder.ts new file mode 100644 index 00000000..1b11f3fb --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/adapters/company-report-profile-finder.ts @@ -0,0 +1,51 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result, type Result as ResultType } from "@repo/rdx-utils"; + +import type { ICompanyPublicServices } from "../../../../../../companies/src/api"; +import type { CompanyReportProfile, ICompanyReportProfileFinder } from "../../../application"; + +export class CompanyReportProfileFinder implements ICompanyReportProfileFinder { + public constructor( + private readonly deps: { + companiesServices: ICompanyPublicServices; + } + ) {} + + public async findByCompanyId( + companyId: UniqueID, + transaction?: unknown + ): Promise, Error>> { + const result = await this.deps.companiesServices.finder.findById({ + companyId, + transaction, + }); + + if (result.isFailure) { + return Result.fail(result.error); + } + + if (result.data.isNone()) { + return Result.ok(Maybe.none()); + } + + const company = result.data.unwrap(); + + const profile: CompanyReportProfile = { + companyId: company.id, + slug: company.slug, + legalName: company.legalName, + tradeName: Maybe.fromNullable(company.tradeName ?? undefined), + tin: Maybe.fromNullable(company.tin ?? undefined), + email: Maybe.fromNullable(company.email ?? undefined), + phone: Maybe.fromNullable(company.phone ?? undefined), + website: Maybe.fromNullable(company.website ?? undefined), + locale: Maybe.none(), + currencyCode: Maybe.none(), + logo: Maybe.none(), + address: Maybe.none(), + version: Maybe.none(), + }; + + return Result.ok(Maybe.some(profile)); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts index 9fabff81..0211c65a 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts @@ -1,27 +1,28 @@ import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api"; import { - ProformaDocumentPipelineFactory, - type ProformaDocumentPipelineFactoryDeps, + ProformaHtmlPreviewPipelineFactory, + type ProformaHtmlPreviewPipelineFactoryDeps, + ProformaPdfDocumentPipelineFactory, + type ProformaPdfDocumentPipelineFactoryDeps, } from "../documents"; -export const buildProformaDocumentService = (params: ModuleParams) => { - const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); +export const buildProformaDocumentServices = (params: ModuleParams) => { + const { documentRenderers, documentStorage } = buildCoreDocumentsDI(params); - const pipelineDeps: ProformaDocumentPipelineFactoryDeps = { + const pdfPipelineDeps: ProformaPdfDocumentPipelineFactoryDeps = { fastReportRenderer: documentRenderers.fastReportRenderer, - - // - signingContextResolver: documentSigning.signingContextResolver, - documentSigningService: documentSigning.signingService, - - // documentStorage: documentStorage.storage, - templateResolver: documentRenderers.fastReportTemplateResolver, }; - const documentGeneratorPipeline = ProformaDocumentPipelineFactory.create(pipelineDeps); + const htmlPreviewPipelineDeps: ProformaHtmlPreviewPipelineFactoryDeps = { + fastReportRenderer: documentRenderers.fastReportRenderer, + templateResolver: documentRenderers.fastReportTemplateResolver, + }; - return documentGeneratorPipeline; + return { + pdf: ProformaPdfDocumentPipelineFactory.create(pdfPipelineDeps), + htmlPreview: ProformaHtmlPreviewPipelineFactory.create(htmlPreviewPipelineDeps), + }; }; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts index d1943716..80463e5a 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts @@ -65,6 +65,7 @@ export function buildProformaPublicServices( taxDefinitionFinder: catalogs.taxDefinition.finder, taxRegimeFinder: catalogs.taxRegime.finder, paymentMethodFinder: catalogs.paymentMethod.finder, + paymentTermFinder: catalogs.paymentTerm.finder, }); const readModelAssemblers = buildProformaReadModelAssemblers({ diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index 024b8973..70997307 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -1,4 +1,5 @@ import { type ModuleParams, buildTransactionManager } from "@erp/core/api"; +import type { ICompanyPublicServices } from "../../../../../../companies/src/api"; import { type ChangeStatusProformaUseCase, @@ -7,13 +8,15 @@ import { type IIssuedInvoicePublicServices, type IssueProformaUseCase, type ListProformasUseCase, - type ReportProformaUseCase, + type PreviewProformaReportUseCase, + type ReportProformaPdfUseCase, type UpdateProformaByIdUseCase, buildChangeStatusProformaUseCase, buildCreateProformaUseCase, buildGetProformaByIdUseCase, buildIssueProformaUseCase, buildListProformasUseCase, + buildPreviewProformaReportUseCase, buildProformaCatalogResolvers, buildProformaCreator, buildProformaFinder, @@ -25,11 +28,12 @@ import { buildProformaStatusChanger, buildProformaToIssuedInvoicePropsConverter, buildProformaUpdater, - buildReportProformaUseCase, + buildReportProformaPdfUseCase, buildUpdateProformaUseCase, } from "../../../application"; -import { buildProformaDocumentService } from "./proforma-documents.di"; +import { CompanyReportProfileFinder } from "../adapters/company-report-profile-finder"; +import { buildProformaDocumentServices } from "./proforma-documents.di"; import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di"; import { buildProformaRepository } from "./proforma-repositories.di"; @@ -39,7 +43,8 @@ export type ProformasInternalDeps = { useCases: { listProformas: () => ListProformasUseCase; getProformaById: () => GetProformaByIdUseCase; - reportProforma: () => ReportProformaUseCase; + reportProformaPdf: () => ReportProformaPdfUseCase; + previewProformaReport: () => PreviewProformaReportUseCase; createProforma: () => CreateProformaUseCase; issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices; @@ -58,6 +63,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter const { database } = params; const catalogs = resolveProformaCatalogsDeps(params); + const companiesServices = params.getService("companies:general"); const transactionManager = buildTransactionManager(database); const persistenceMappers = buildProformaPersistenceMappers(); @@ -113,7 +119,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter paymentMethodFinder: catalogs.paymentMethod.finder, }); - const documentGeneratorPipeline = buildProformaDocumentService(params); + const companyReportProfileFinder = new CompanyReportProfileFinder({ + companiesServices, + }); + const documentGeneratorServices = buildProformaDocumentServices(params); return { useCases: { @@ -150,13 +159,25 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter transactionManager, }), - reportProforma: () => - buildReportProformaUseCase({ + reportProformaPdf: () => + buildReportProformaPdfUseCase({ finder, + companyReportProfileFinder, fullReadModelAssembler: readModelAssemblers.full, fullSnapshotBuilder: snapshotBuilders.full, reportSnapshotBuilder: snapshotBuilders.report, - documentService: documentGeneratorPipeline, + documentService: documentGeneratorServices.pdf, + transactionManager, + }), + + previewProformaReport: () => + buildPreviewProformaReportUseCase({ + finder, + companyReportProfileFinder, + fullReadModelAssembler: readModelAssemblers.full, + fullSnapshotBuilder: snapshotBuilders.full, + reportSnapshotBuilder: snapshotBuilders.report, + documentService: documentGeneratorServices.htmlPreview, transactionManager, }), diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts index d368a4ef..006a9a3f 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts @@ -1 +1,2 @@ -export * from "./proforma-document-pipeline-factory.ts"; +export * from "./proforma-html-preview-pipeline-factory.ts"; +export * from "./proforma-pdf-document-pipeline-factory.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts deleted file mode 100644 index fec8dcb7..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - DocumentGenerationService, - DocumentPostProcessorChain, - type FastReportRenderer, - type FastReportTemplateResolver, - type IDocumentPostProcessor, - type IDocumentSideEffect, - type IDocumentSigningService, - type IDocumentStorage, - type ISigningContextResolver, -} from "@erp/core/api"; - -import { - type ProformaDocumentGeneratorService, - ProformaDocumentMetadataFactory, - ProformaDocumentPropertiesFactory, - type ProformaReportSnapshot, -} from "../../../../application"; -import { ProformaSignedDocumentCachePreProcessor } from "../pre-processors"; -import { ProformaDocumentRenderer } from "../renderers"; -import { PersistProformaDocumentSideEffect } from "../side-effects"; - -/** - * Factory de pipeline de generación de documentos - * para facturas emitidas (Issued Invoice). - */ - -export interface ProformaDocumentPipelineFactoryDeps { - // Core / Infra - fastReportRenderer: FastReportRenderer; - templateResolver: FastReportTemplateResolver; - - signingContextResolver: ISigningContextResolver; - documentSigningService: IDocumentSigningService; - - documentStorage: IDocumentStorage; -} - -export class ProformaDocumentPipelineFactory { - static create(deps: ProformaDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService { - // 1. Pre-processors (cache firmado) - const preProcessors = [new ProformaSignedDocumentCachePreProcessor(deps.documentStorage)]; - - // 2. Renderer (FastReport) - const renderer = new ProformaDocumentRenderer(deps.fastReportRenderer, deps.templateResolver); - - // 3) Metadata and properties factory (Application) - const metadataFactory = new ProformaDocumentMetadataFactory(); - const propertiesFactory = new ProformaDocumentPropertiesFactory(); - - // 3) Firma real (Core / Infra) - const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([ - // Aquí podrían ir más post-procesadores, como uno de validación o similar - ]); - - // 4. Side-effects (persistencia best-effort) - const sideEffects: IDocumentSideEffect[] = [ - new PersistProformaDocumentSideEffect(deps.documentStorage), - ]; - - // 5. Pipeline final - return new DocumentGenerationService({ - renderer, - preProcessors, - postProcessor, - sideEffects, - metadataFactory, - propertiesFactory, - }); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-html-preview-pipeline-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-html-preview-pipeline-factory.ts new file mode 100644 index 00000000..84766e29 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-html-preview-pipeline-factory.ts @@ -0,0 +1,40 @@ +import { + DocumentGenerationService, + DocumentPostProcessorChain, + type FastReportRenderer, + type FastReportTemplateResolver, +} from "@erp/core/api"; + +import { + type ProformaDocumentGeneratorService, + ProformaDocumentPropertiesFactory, + type ProformaReportSnapshot, +} from "../../../../application"; +import { ProformaHtmlPreviewRenderer } from "../renderers"; +import { ProformaHtmlPreviewDocumentMetadataFactory } from "../services/proforma-html-preview-document-metadata-factory"; + +export interface ProformaHtmlPreviewPipelineFactoryDeps { + fastReportRenderer: FastReportRenderer; + templateResolver: FastReportTemplateResolver; +} + +export class ProformaHtmlPreviewPipelineFactory { + static create(deps: ProformaHtmlPreviewPipelineFactoryDeps): ProformaDocumentGeneratorService { + const renderer = new ProformaHtmlPreviewRenderer( + deps.fastReportRenderer, + deps.templateResolver + ); + const metadataFactory = new ProformaHtmlPreviewDocumentMetadataFactory(); + const propertiesFactory = new ProformaDocumentPropertiesFactory(); + const postProcessor = new DocumentPostProcessorChain([]); + + return new DocumentGenerationService({ + renderer, + preProcessors: [], + postProcessor, + sideEffects: [], + metadataFactory, + propertiesFactory, + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-pdf-document-pipeline-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-pdf-document-pipeline-factory.ts new file mode 100644 index 00000000..bcd833d5 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-pdf-document-pipeline-factory.ts @@ -0,0 +1,49 @@ +import { + DocumentGenerationService, + DocumentPostProcessorChain, + type FastReportRenderer, + type FastReportTemplateResolver, + type IDocumentSideEffect, + type IDocumentStorage, +} from "@erp/core/api"; + +import { + type ProformaDocumentGeneratorService, + ProformaDocumentPropertiesFactory, + type ProformaReportSnapshot, +} from "../../../../application"; +import { ProformaPdfCachePreProcessor } from "../pre-processors"; +import { ProformaPdfDocumentRenderer } from "../renderers"; +import { ProformaPdfDocumentMetadataFactory } from "../services/proforma-pdf-document-metadata-factory"; +import { PersistProformaPdfSideEffect } from "../side-effects"; + +export interface ProformaPdfDocumentPipelineFactoryDeps { + fastReportRenderer: FastReportRenderer; + templateResolver: FastReportTemplateResolver; + documentStorage: IDocumentStorage; +} + +export class ProformaPdfDocumentPipelineFactory { + static create(deps: ProformaPdfDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService { + const preProcessors = [new ProformaPdfCachePreProcessor(deps.documentStorage)]; + const renderer = new ProformaPdfDocumentRenderer( + deps.fastReportRenderer, + deps.templateResolver + ); + const metadataFactory = new ProformaPdfDocumentMetadataFactory(); + const propertiesFactory = new ProformaDocumentPropertiesFactory(); + const postProcessor = new DocumentPostProcessorChain([]); + const sideEffects: IDocumentSideEffect[] = [ + new PersistProformaPdfSideEffect(deps.documentStorage), + ]; + + return new DocumentGenerationService({ + renderer, + preProcessors, + postProcessor, + sideEffects, + metadataFactory, + propertiesFactory, + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts index 5ed1dd1b..6fc15696 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts @@ -1 +1 @@ -export * from "./proforma-document-cache-pre-processor"; +export * from "./proforma-pdf-cache-pre-processor"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts deleted file mode 100644 index ffdbc011..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - DocumentStorageKeyFactory, - type IDocument, - type IDocumentMetadata, - type IDocumentPreProcessor, - type IDocumentStorage, - logger, -} from "@erp/core/api"; - -/** - * Pre-processor de cache técnico para documentos firmados - * de facturas emitidas. - * - * - Best-effort - * - Nunca rompe el flujo - * - Invalida cache corrupto - */ -export class ProformaSignedDocumentCachePreProcessor implements IDocumentPreProcessor { - constructor(private readonly docStorage: IDocumentStorage) {} - - async tryResolve(metadata: IDocumentMetadata): Promise { - const metadataRecord = metadata as unknown as Record; - try { - const { paths, storageKey } = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord); - - if (!storageKey) { - return null; - } - - const exists = await this.docStorage.existsKeyStorage(storageKey, paths); - - if (!exists) { - return null; - } - - logger.info(`✅ Found Server cached document for key ${storageKey}`); - - const document = await this.docStorage.readDocument(storageKey, paths); - - if (!this.isValid(document)) { - logger.warn(`Corrupted or invalid cached document for key ${storageKey}`, { - label: "ProformaSignedDocumentCachePreProcessor", - }); - return null; - } - - return document; - } catch { - // best-effort: cualquier fallo se trata como cache miss - return null; - } - } - - /** - * Validación mínima de integridad. - * No valida firma criptográfica. - */ - private isValid(document: IDocument | null): boolean { - if (!document) return false; - - if (!document.payload || document.payload.length === 0) { - return false; - } - - if (document.mimeType !== "application/pdf") { - return false; - } - - return true; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-pdf-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-pdf-cache-pre-processor.ts new file mode 100644 index 00000000..f6b372ee --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-pdf-cache-pre-processor.ts @@ -0,0 +1,34 @@ +import { + DocumentStorageKeyFactory, + type IDocument, + type IDocumentMetadata, + type IDocumentPreProcessor, + type IDocumentStorage, +} from "@erp/core/api"; + +export class ProformaPdfCachePreProcessor implements IDocumentPreProcessor { + public constructor(private readonly docStorage: IDocumentStorage) {} + + public async tryResolve(metadata: IDocumentMetadata): Promise { + const metadataRecord = metadata as unknown as Record; + const { paths, storageKey } = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord); + + if (!storageKey) { + return null; + } + + const exists = await this.docStorage.existsKeyStorage(storageKey, paths); + + if (!exists) { + return null; + } + + const document = await this.docStorage.readDocument(storageKey, paths); + + if (!document || document.mimeType !== "application/pdf" || document.payload.length === 0) { + return null; + } + + return document; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts index 72949b33..5f931861 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts @@ -1 +1,2 @@ -export * from "./proforma-document-renderer.ts"; +export * from "./proforma-html-preview-renderer.ts"; +export * from "./proforma-pdf-document-renderer.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts deleted file mode 100644 index 900f0a66..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { - FastReportRenderer, - FastReportTemplateResolver, - IDocument, - IDocumentProperties, - IDocumentRenderer, -} from "@erp/core/api"; - -import type { ProformaReportSnapshot } from "../../../../../application"; - -/** - * Adaptador Application → Infra para la generación del documento - * PDF de una factura emitida (Issued Invoice). - * - * - Recibe snapshot de report - * - Invoca FastReportRenderer - * - Devuelve IDocument - * - * NO captura errores: FastReportError se propaga. - */ - -export type ProformaDocumentRenderParams = { - companySlug: string; - format: string; - languageCode: string; - filename: string; - mimeType: string; - properties: IDocumentProperties; -}; - -export class ProformaDocumentRenderer implements IDocumentRenderer { - constructor( - private readonly fastReportRenderer: FastReportRenderer, - private readonly templateResolver: FastReportTemplateResolver - ) {} - - async render( - snapshot: ProformaReportSnapshot, - params: ProformaDocumentRenderParams - ): Promise { - const { companySlug, format, languageCode, filename, mimeType, properties } = params; - - // Template - const templatePath = this.templateResolver.resolveTemplatePath({ - module: "customer-invoices", - companySlug, - languageCode, - templateFilename: "proforma.frx", - }); - - const output = await this.fastReportRenderer.render({ - templatePath, - inputData: snapshot, - format, - properties, - }); - - return { - payload: this.normalizePayload(output.payload), - mimeType, - filename, - }; - } - - /** - * Normaliza la salida de FastReport a Buffer. - * FastReport puede devolver string o Buffer. - */ - private normalizePayload(payload: Buffer | string): Buffer { - return Buffer.isBuffer(payload) ? payload : Buffer.from(payload); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-html-preview-renderer.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-html-preview-renderer.ts new file mode 100644 index 00000000..bb5a9788 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-html-preview-renderer.ts @@ -0,0 +1,50 @@ +import type { + FastReportRenderer, + FastReportTemplateResolver, + IDocument, + IDocumentProperties, + IDocumentRenderer, +} from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../../../../../application"; + +type ProformaHtmlPreviewRenderParams = { + companySlug: string; + format: string; + languageCode: string; + filename: string; + mimeType: string; + properties: IDocumentProperties; +}; + +export class ProformaHtmlPreviewRenderer implements IDocumentRenderer { + public constructor( + private readonly fastReportRenderer: FastReportRenderer, + private readonly templateResolver: FastReportTemplateResolver + ) {} + + public async render( + snapshot: ProformaReportSnapshot, + params: ProformaHtmlPreviewRenderParams + ): Promise { + const templatePath = this.templateResolver.resolveTemplatePath({ + module: "customer-invoices", + companySlug: params.companySlug, + languageCode: params.languageCode, + templateFilename: "proforma.frx", + }); + + const output = await this.fastReportRenderer.render({ + templatePath, + inputData: snapshot, + format: params.format, + properties: params.properties, + }); + + return { + payload: Buffer.isBuffer(output.payload) ? output.payload : Buffer.from(output.payload), + mimeType: params.mimeType, + filename: params.filename, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-pdf-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-pdf-document-renderer.ts new file mode 100644 index 00000000..ad99d542 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-pdf-document-renderer.ts @@ -0,0 +1,50 @@ +import type { + FastReportRenderer, + FastReportTemplateResolver, + IDocument, + IDocumentProperties, + IDocumentRenderer, +} from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../../../../../application"; + +type ProformaPdfDocumentRenderParams = { + companySlug: string; + format: string; + languageCode: string; + filename: string; + mimeType: string; + properties: IDocumentProperties; +}; + +export class ProformaPdfDocumentRenderer implements IDocumentRenderer { + public constructor( + private readonly fastReportRenderer: FastReportRenderer, + private readonly templateResolver: FastReportTemplateResolver + ) {} + + public async render( + snapshot: ProformaReportSnapshot, + params: ProformaPdfDocumentRenderParams + ): Promise { + const templatePath = this.templateResolver.resolveTemplatePath({ + module: "customer-invoices", + companySlug: params.companySlug, + languageCode: params.languageCode, + templateFilename: "proforma.frx", + }); + + const output = await this.fastReportRenderer.render({ + templatePath, + inputData: snapshot, + format: params.format, + properties: params.properties, + }); + + return { + payload: Buffer.isBuffer(output.payload) ? output.payload : Buffer.from(output.payload), + mimeType: params.mimeType, + filename: params.filename, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-html-preview-document-metadata-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-html-preview-document-metadata-factory.ts new file mode 100644 index 00000000..0158c9b5 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-html-preview-document-metadata-factory.ts @@ -0,0 +1,19 @@ +import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../../../../application"; + +export class ProformaHtmlPreviewDocumentMetadataFactory + implements IDocumentMetadataFactory +{ + build(snapshot: ProformaReportSnapshot): IDocumentMetadata { + return { + documentType: "proforma", + documentId: snapshot.id, + companyId: snapshot.company_id, + companySlug: snapshot.company.slug, + format: "HTML", + languageCode: snapshot.localization.languageCode, + filename: `proforma-${snapshot.series}${snapshot.invoice_number}.html`, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-pdf-document-metadata-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-pdf-document-metadata-factory.ts new file mode 100644 index 00000000..c83d2f22 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-pdf-document-metadata-factory.ts @@ -0,0 +1,48 @@ +import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../../../../application"; + +export class ProformaPdfDocumentMetadataFactory + implements IDocumentMetadataFactory +{ + build(snapshot: ProformaReportSnapshot): IDocumentMetadata { + return { + documentType: "proforma", + documentId: snapshot.id, + companyId: snapshot.company_id, + companySlug: snapshot.company.slug, + format: "PDF", + languageCode: snapshot.localization.languageCode, + filename: `proforma-${snapshot.series}${snapshot.invoice_number}.pdf`, + storageKey: this.buildStorageKey(snapshot), + }; + } + + private buildStorageKey(snapshot: ProformaReportSnapshot): string | undefined { + if (snapshot.status === "draft") { + return undefined; + } + + // La cache de proformas no draft es segura respecto al contenido de la proforma + // porque el dominio no permite modificar proformas fuera de draft. Queda pendiente + // invalidar por version de plantilla/branding cuando esos datos existan. + const keyParts = [ + "proforma", + snapshot.company_id, + snapshot.id, + snapshot.status, + "pdf", + snapshot.localization.languageCode, + snapshot.localization.currencyCode, + snapshot.company.slug, + ]; + + const companyVersion = snapshot.company.version.getOrUndefined(); + + if (companyVersion) { + keyParts.push(companyVersion); + } + + return keyParts.join(":"); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts index 75010823..cbf6c3e3 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts @@ -1 +1 @@ -export * from "./persist-proforma-document-side-effect"; +export * from "./persist-proforma-pdf-side-effect"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts deleted file mode 100644 index a032cdbb..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { - IDocument, - IDocumentMetadata, - IDocumentSideEffect, - IDocumentStorage, -} from "@erp/core/api"; - -/** - * Side-effect de persistencia best-effort del documento final - * de una factura emitida. - * - * - Nunca rompe el flujo - * - Usa cacheKey/metadata para decidir la clave - */ -export class PersistProformaDocumentSideEffect implements IDocumentSideEffect { - constructor(private readonly storage: IDocumentStorage) {} - - async execute(document: IDocument, metadata: IDocumentMetadata): Promise { - // Si no hay cacheKey, no se persiste - if (!metadata.storageKey) { - return; - } - - // Persistencia best-effort - await this.storage.saveDocument(document, metadata as unknown as Record); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-pdf-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-pdf-side-effect.ts new file mode 100644 index 00000000..1aff0d41 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-pdf-side-effect.ts @@ -0,0 +1,18 @@ +import type { + IDocument, + IDocumentMetadata, + IDocumentSideEffect, + IDocumentStorage, +} from "@erp/core/api"; + +export class PersistProformaPdfSideEffect implements IDocumentSideEffect { + public constructor(private readonly storage: IDocumentStorage) {} + + public async execute(document: IDocument, metadata: IDocumentMetadata): Promise { + if (!metadata.storageKey) { + return; + } + + await this.storage.saveDocument(document, metadata as unknown as Record); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts index e8049c3f..ee66f7a1 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts @@ -4,5 +4,6 @@ export * from "./get-proforma.controller"; export * from "./issue-proforma.controller"; export * from "./list-proformas.controller"; -export * from "./report-proforma.controller"; +export * from "./preview-proforma-report.controller"; +export * from "./report-proforma-pdf.controller"; export * from "./update-proforma.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/preview-proforma-report.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/preview-proforma-report.controller.ts new file mode 100644 index 00000000..ba7ba4bf --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/preview-proforma-report.controller.ts @@ -0,0 +1,41 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { PreviewProformaReportUseCase } from "../../../../application"; +import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; + +export class PreviewProformaReportController extends ExpressController { + public constructor(private readonly useCase: PreviewProformaReportUseCase) { + super(); + this.errorMapper = proformasApiErrorMapper; + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { proforma_id } = this.req.params; + + const result = await this.useCase.execute({ + proforma_id, + companyId, + }); + + return result.match( + ({ payload }) => this.downloadHTML(payload), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma-pdf.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma-pdf.controller.ts new file mode 100644 index 00000000..7d825f02 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma-pdf.controller.ts @@ -0,0 +1,41 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { ReportProformaPdfUseCase } from "../../../../application"; +import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; + +export class ReportProformaPdfController extends ExpressController { + public constructor(private readonly useCase: ReportProformaPdfUseCase) { + super(); + this.errorMapper = proformasApiErrorMapper; + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { proforma_id } = this.req.params; + + const result = await this.useCase.execute({ + proforma_id, + companyId, + }); + + return result.match( + ({ payload, filename }) => this.downloadPDF(payload, String(filename)), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma.controller.ts deleted file mode 100644 index 013e0a5a..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/report-proforma.controller.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ExpressController, - type RendererFormat, - forbidQueryFieldGuard, - requireAuthenticatedGuard, - requireCompanyContextGuard, -} from "@erp/core/api"; - -import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common"; -import type { ReportProformaUseCase } from "../../../../application/index.ts"; -import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; - -export class ReportProformaController extends ExpressController { - public constructor(private readonly useCase: ReportProformaUseCase) { - super(); - this.errorMapper = proformasApiErrorMapper; - - // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards( - requireAuthenticatedGuard(), - requireCompanyContextGuard(), - forbidQueryFieldGuard("companyId") - ); - } - - protected async executeImpl() { - const companyId = this.getTenantId(); - if (!companyId) { - return this.forbiddenError("Tenant ID not found"); - } - - const { proforma_id } = this.req.params; - const { format } = this.req.query as ReportProformaByIdQueryRequestDTO; - - const result = await this.useCase.execute({ - proforma_id, - companyId, - format: format as RendererFormat, - }); - - return result.match( - ({ payload, filename }) => { - if (format === "PDF") { - return this.downloadPDF(payload as Buffer, String(filename)); - } - if (format === "HTML") { - return this.downloadHTML(payload as unknown as string); - } - // JSON - return this.json(payload); - }, - (err) => this.handleError(err) - ); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas-api-error-mapper.ts index 600586f2..fe0c6b06 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas-api-error-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas-api-error-mapper.ts @@ -5,6 +5,7 @@ import { ValidationApiError, } from "@erp/core/api"; +import { CompanyReportProfileNotFoundError, isCompanyReportProfileNotFoundError } from "../../../application"; import { type CustomerInvoiceIdAlreadyExistsError, type EntityIsNotProformaError, @@ -77,8 +78,19 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = { ), }; +const companyReportProfileNotFoundRule: ErrorToApiRule = { + priority: 130, + matches: (e) => isCompanyReportProfileNotFoundError(e), + build: (e) => + new ConflictApiError( + (e as CompanyReportProfileNotFoundError).message || + "Cannot generate the proforma report because the company document profile is missing." + ), +}; + // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() + .register(companyReportProfileNotFoundRule) .register(proformaDuplicateRule) .register(proformaItemMismatchError) .register(entityIsNotProformaError) diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index 08dc53c5..41516a5b 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -6,8 +6,9 @@ import { GetProformaController, IssueProformaController, ListProformasController, + PreviewProformaReportController, type ProformasInternalDeps, - ReportProformaController, + ReportProformaPdfController, } from ".."; import { @@ -17,8 +18,8 @@ import { GetProformaByIdRequestSchema, IssueProformaByIdParamsRequestSchema, ListProformasRequestSchema, - ReportProformaByIdParamsRequestSchema, - ReportProformaByIdQueryRequestSchema, + PreviewProformaReportByIdParamsRequestSchema, + ReportProformaPdfByIdParamsRequestSchema, UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, } from "../../../../common"; @@ -70,14 +71,21 @@ export const proformasRouter = (params: StartParams) => { ); router.get( - "/:proforma_id/report", - //checkTabContext, - validateRequest(ReportProformaByIdParamsRequestSchema, "params"), - validateRequest(ReportProformaByIdQueryRequestSchema, "query"), - + "/:proforma_id/report/pdf", + validateRequest(ReportProformaPdfByIdParamsRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.reportProforma(); - const controller = new ReportProformaController(useCase); + const useCase = deps.useCases.reportProformaPdf(); + const controller = new ReportProformaPdfController(useCase); + return controller.execute(req, res, next); + } + ); + + router.get( + "/:proforma_id/report/preview", + validateRequest(PreviewProformaReportByIdParamsRequestSchema, "params"), + (req: Request, res: Response, next: NextFunction) => { + const useCase = deps.useCases.previewProformaReport(); + const controller = new PreviewProformaReportController(useCase); return controller.execute(req, res, next); } ); diff --git a/modules/customer-invoices/src/common/dto/request/proformas/index.ts b/modules/customer-invoices/src/common/dto/request/proformas/index.ts index d39b3dda..e89b4ad0 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/index.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/index.ts @@ -4,5 +4,6 @@ export * from "./delete-proforma-by-id.request.dto"; export * from "./get-proforma-by-id.request.dto"; export * from "./issue-proforma-by-id.request.dto"; export * from "./list-proformas.request.dto"; -export * from "./report-proforma-by-id.request.dto"; +export * from "./preview-proforma-report-by-id.request.dto"; +export * from "./report-proforma-pdf-by-id.request.dto"; export * from "./update-proforma-by-id.request.dto"; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/preview-proforma-report-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/preview-proforma-report-by-id.request.dto.ts new file mode 100644 index 00000000..c5d18aab --- /dev/null +++ b/modules/customer-invoices/src/common/dto/request/proformas/preview-proforma-report-by-id.request.dto.ts @@ -0,0 +1,9 @@ +import { z } from "zod/v4"; + +export const PreviewProformaReportByIdParamsRequestSchema = z.object({ + proforma_id: z.string(), +}); + +export type PreviewProformaReportByIdParamsRequestDTO = z.infer< + typeof PreviewProformaReportByIdParamsRequestSchema +>; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts deleted file mode 100644 index a6711949..00000000 --- a/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from "zod/v4"; - -export const ReportProformaByIdParamsRequestSchema = z.object({ - proforma_id: z.string(), -}); - -export type ReportProformaByIdParamsRequestDTO = z.infer< - typeof ReportProformaByIdParamsRequestSchema ->; -export const ReportProformaByIdQueryRequestSchema = z.object({ - format: z - .string() - .default("pdf") - .transform((v) => v.trim().toLowerCase()) - .pipe(z.enum(["pdf", "html", "json"])) - .transform((v) => v.toUpperCase() as "PDF" | "HTML" | "JSON"), -}); - -export type ReportProformaByIdQueryRequestDTO = z.infer< - typeof ReportProformaByIdQueryRequestSchema ->; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-pdf-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-pdf-by-id.request.dto.ts new file mode 100644 index 00000000..26f9a7fb --- /dev/null +++ b/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-pdf-by-id.request.dto.ts @@ -0,0 +1,9 @@ +import { z } from "zod/v4"; + +export const ReportProformaPdfByIdParamsRequestSchema = z.object({ + proforma_id: z.string(), +}); + +export type ReportProformaPdfByIdParamsRequestDTO = z.infer< + typeof ReportProformaPdfByIdParamsRequestSchema +>; diff --git a/modules/customer-invoices/src/common/dto/response/proformas/index.ts b/modules/customer-invoices/src/common/dto/response/proformas/index.ts index 4235dabe..abf2fa1f 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/index.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/index.ts @@ -2,5 +2,4 @@ export * from "./create-proforma.response.dto"; export * from "./get-proforma-by-id.response.dto"; export * from "./issue-proforma-by-id.response.dto"; export * from "./list-proformas.response.dto"; -export * from "./report-proforma-by-id.response.dto"; export * from "./update-proformas-by-id.response.dto"; diff --git a/modules/customer-invoices/src/common/dto/response/proformas/report-proforma-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/report-proforma-by-id.response.dto.ts deleted file mode 100644 index eb0f31e1..00000000 --- a/modules/customer-invoices/src/common/dto/response/proformas/report-proforma-by-id.response.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export type ReportProformaByIdResponseDTO = Blob; diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx index 8d599e24..b71d6360 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx @@ -2,7 +2,9 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components"; import { DateHelper } from "@repo/rdx-utils"; import { Button, + ButtonGroup, Checkbox, + Spinner, Tooltip, TooltipContent, TooltipProvider, @@ -11,6 +13,7 @@ import { import type { ColumnDef } from "@tanstack/react-table"; import { ExternalLinkIcon, + FileDownIcon, FileTextIcon, PencilIcon, RefreshCwIcon, @@ -39,6 +42,10 @@ type GridActionHandlers = { canIssue?: (proforma: ProformaListRow) => boolean; canEdit?: (proforma: ProformaListRow) => boolean; canDelete?: (proforma: ProformaListRow) => boolean; + + onDownloadPdf?: (proforma: ProformaListRow) => void; + pdfDownloadingId?: string | null; + isPdfDownloading?: boolean; }; export function useProformasGridColumns( @@ -269,8 +276,12 @@ export function useProformasGridColumns( const availableTransitions = PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? []; + const isPDFLoading = + actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id; + const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation(); + return ( -
+ {!isIssued && actionHandlers.onEditClick && ( @@ -352,6 +363,34 @@ export function useProformasGridColumns( )} + {/* Descargar en PDF */} + + + { + stop(e); + actionHandlers.onDownloadPdf?.(proforma); + }} + size="icon" + variant="ghost" + > + {isPDFLoading ? ( + + ) : ( + + )} + Descargar PDF + + } + /> + Descargar PDF + + + {/* Eliminar */} {!isIssued && actionHandlers.onDeleteClick && ( @@ -377,7 +416,7 @@ export function useProformasGridColumns( )} -
+ ); }, }, diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx index b27a600e..da1a1def 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx @@ -64,6 +64,8 @@ export const ListProformasPage = () => { deleteDialogCtrl.openDialog(prepareDeleteProformaTargets(proformaRow)), onChangeStatusClick: (proforma) => changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(proforma)), + + onDownloadPdf: (proforma) => null, }); const isPanelOpen = panelCtrl.panelState.isOpen; diff --git a/modules/customer-invoices/src/web/proformas/shared/api/report-proforma-by-id.api.ts b/modules/customer-invoices/src/web/proformas/shared/api/report-proforma-by-id.api.ts index 8f9679d6..d0902029 100644 --- a/modules/customer-invoices/src/web/proformas/shared/api/report-proforma-by-id.api.ts +++ b/modules/customer-invoices/src/web/proformas/shared/api/report-proforma-by-id.api.ts @@ -1,35 +1,22 @@ import type { IDataSource } from "@erp/core/client"; -import type { - ReportProformaByIdQueryRequestDTO, - ReportProformaByIdResponseDTO, -} from "../../../../common"; - -export type ProformaReportFormat = "PDF" | "HTML" | "JSON"; - export interface ReportProformaByIdParams { id: string; - data: ReportProformaByIdQueryRequestDTO; } -export type ReportProformaByIdResult = ReportProformaByIdResponseDTO; +export type ReportProformaByIdResult = Blob; export const reportProformaById = ( dataSource: IDataSource, params: ReportProformaByIdParams ): Promise => { - const { - id, - data: { format = "PDF" }, - } = params; + const { id } = params; if (!id) throw new Error("proformaId is required"); - return dataSource.custom({ - path: `proformas/${id}/report`, + return dataSource.custom({ + path: `proformas/${id}/report/pdf`, method: "get", - data: { - format, - }, + data: undefined, }); };