From fc8c7fea99c020f9e0d0489c4e8794655c471abf Mon Sep 17 00:00:00 2001 From: David Date: Mon, 21 Sep 2026 16:58:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(proformas):=20agregar=20contexto=20de=20ge?= =?UTF-8?q?neraci=C3=B3n=20de=20documentos=20y=20actualizar=20f=C3=A1brica?= =?UTF-8?q?s=20y=20servicios=20relacionados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/document-generation-service.ts | 33 +++-- .../document-metadata-factory.interface.ts | 2 +- .../document-properties-factory.interface.ts | 4 +- .../services/document-renderer.interface.ts | 4 +- .../proformas/di/proforma-use-cases.di.ts | 3 +- .../application/proformas/services/index.ts | 1 + ...a-document-generation-context.interface.ts | 17 +++ .../proforma-document-generator.interface.ts | 7 +- .../proforma-document-properties-factory.ts | 11 +- .../preview-proforma-report.use-case.test.ts | 48 ++++++- .../preview-proforma-report.use-case.ts | 4 +- .../report-proforma-pdf.use-case.test.ts | 58 ++++++++- .../use-cases/report-proforma-pdf.use-case.ts | 10 +- .../proforma-html-preview-pipeline-factory.ts | 19 +-- .../proforma-pdf-document-pipeline-factory.ts | 10 +- .../proforma-html-preview-renderer.ts | 15 ++- .../proforma-pdf-document-renderer.ts | 15 ++- ...oforma-document-metadata-factories.test.ts | 117 ++++++++++++++++++ ...-html-preview-document-metadata-factory.ts | 16 ++- .../proforma-pdf-document-metadata-factory.ts | 34 ++--- 20 files changed, 357 insertions(+), 71 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/proformas/services/proforma-document-generation-context.interface.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-document-metadata-factories.test.ts diff --git a/modules/core/src/api/application/documents/services/document-generation-service.ts b/modules/core/src/api/application/documents/services/document-generation-service.ts index eb79c900..34106324 100644 --- a/modules/core/src/api/application/documents/services/document-generation-service.ts +++ b/modules/core/src/api/application/documents/services/document-generation-service.ts @@ -21,26 +21,35 @@ import type { IDocumentSideEffect } from "./document-side-effect.interface"; * 5. Side-effects (persistencia, métricas) [best-effort] */ -export type DocumentGenerationServiceDeps = { - renderer: IDocumentRenderer; - metadataFactory: IDocumentMetadataFactory; - propertiesFactory: IDocumentPropertiesFactory; +export type DocumentGenerationServiceDeps> = { + renderer: IDocumentRenderer; + metadataFactory: IDocumentMetadataFactory; + propertiesFactory: IDocumentPropertiesFactory; preProcessors: readonly IDocumentPreProcessor[]; postProcessor: IDocumentPostProcessor; sideEffects: readonly IDocumentSideEffect[]; }; -export type DocumentGenerationServiceRenderParams = Record; +export type DocumentGenerationServiceRendererParams = { + format: "PDF" | "HTML"; + languageCode: string; + filename: string; + mimeType: string; + properties: IDocumentProperties; +}; -export class DocumentGenerationService { - private readonly renderer: IDocumentRenderer; - private readonly metadataFactory: IDocumentMetadataFactory; - private readonly propertiesFactory: IDocumentPropertiesFactory; +export class DocumentGenerationService> { + private readonly renderer: IDocumentRenderer< + TSnapshot, + TContext & DocumentGenerationServiceRendererParams + >; + private readonly metadataFactory: IDocumentMetadataFactory; + private readonly propertiesFactory: IDocumentPropertiesFactory; private readonly preProcessors: readonly IDocumentPreProcessor[]; private readonly postProcessor: IDocumentPostProcessor; private readonly sideEffects: readonly IDocumentSideEffect[]; - constructor(deps: DocumentGenerationServiceDeps) { + constructor(deps: DocumentGenerationServiceDeps) { this.renderer = deps.renderer; this.metadataFactory = deps.metadataFactory; this.propertiesFactory = deps.propertiesFactory; @@ -51,7 +60,7 @@ export class DocumentGenerationService { async generate( snapshot: TSnapshot, - params: DocumentGenerationServiceRenderParams + params: TContext ): Promise> { let metadata: IDocumentMetadata; let properties: IDocumentProperties; @@ -64,7 +73,7 @@ export class DocumentGenerationService { } try { - properties = this.propertiesFactory.build(snapshot); + properties = this.propertiesFactory.build(snapshot, params); } catch (error) { return Result.fail(DocumentGenerationError.properties(error)); } diff --git a/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts b/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts index 5bd44a34..9da1ef5a 100644 --- a/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts +++ b/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts @@ -1,5 +1,5 @@ import type { IDocumentMetadata } from "../application-models"; -export interface IDocumentMetadataFactory> { +export interface IDocumentMetadataFactory> { build(snapshot: TSnapshot, context: TSnapshotContext): IDocumentMetadata; } diff --git a/modules/core/src/api/application/documents/services/document-properties-factory.interface.ts b/modules/core/src/api/application/documents/services/document-properties-factory.interface.ts index 9917511c..2f3acb07 100644 --- a/modules/core/src/api/application/documents/services/document-properties-factory.interface.ts +++ b/modules/core/src/api/application/documents/services/document-properties-factory.interface.ts @@ -1,5 +1,5 @@ import type { IDocumentProperties } from "../application-models"; -export interface IDocumentPropertiesFactory { - build(snapshot: TSnapshot): IDocumentProperties; +export interface IDocumentPropertiesFactory> { + build(snapshot: TSnapshot, context: TContext): IDocumentProperties; } diff --git a/modules/core/src/api/application/documents/services/document-renderer.interface.ts b/modules/core/src/api/application/documents/services/document-renderer.interface.ts index 7c5ff186..23fb302e 100644 --- a/modules/core/src/api/application/documents/services/document-renderer.interface.ts +++ b/modules/core/src/api/application/documents/services/document-renderer.interface.ts @@ -1,5 +1,5 @@ import type { IDocument } from "../application-models"; -export interface IDocumentRenderer { - render(source: TSource, params?: Record): Promise; +export interface IDocumentRenderer> { + render(source: TSource, params: TRenderParams): Promise; } 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 4602a996..da3d82c9 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 @@ -18,6 +18,7 @@ import type { IProformaStatusChanger, IProformaUpdater, ProformaDocumentGeneratorService, + ProformaPdfDocumentGenerationContext, } from "../services"; import type { IIssueProformaResponseSnapshotBuilder, @@ -88,7 +89,7 @@ export function buildReportProformaPdfUseCase(deps: { fullReadModelAssembler: IProformaFullReadModelAssembler; fullSnapshotBuilder: IProformaFullSnapshotBuilder; reportSnapshotBuilder: IProformaReportSnapshotBuilder; - documentService: ProformaDocumentGeneratorService; + documentService: ProformaDocumentGeneratorService; transactionManager: ITransactionManager; }) { return new ReportProformaPdfUseCase({ 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 2ee74bdf..de50be93 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -4,6 +4,7 @@ export * from "./company-report-profile-finder"; export * from "./proforma-archiver"; export * from "./proforma-creator"; export * from "./proforma-deleter"; +export * from "./proforma-document-generation-context.interface"; export * from "./proforma-document-generator.interface"; export * from "./proforma-document-properties-factory"; export * from "./proforma-duplicator"; diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generation-context.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generation-context.interface.ts new file mode 100644 index 00000000..949c4050 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generation-context.interface.ts @@ -0,0 +1,17 @@ +/** + * Datos externos al snapshot necesarios para resolver y generar una proforma. + * + * Los identificadores funcionales permanecen en el snapshot para evitar + * duplicar fuentes de verdad entre el caso de uso y el pipeline. + */ +export interface ProformaDocumentGenerationContext { + readonly companySlug: string; + readonly languageCode: string; + readonly companyLegalName: string; +} + +/** Datos adicionales requeridos exclusivamente por el pipeline PDF y su cache. */ +export interface ProformaPdfDocumentGenerationContext extends ProformaDocumentGenerationContext { + readonly currencyCode: string; + readonly companyVersion?: string; +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts index aaefdfb0..d80a1f6b 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts @@ -2,5 +2,8 @@ import type { DocumentGenerationService } from "@erp/core/api"; import type { ProformaReportSnapshot } from "../snapshot-builders"; -export interface ProformaDocumentGeneratorService - extends DocumentGenerationService {} +import type { ProformaDocumentGenerationContext } from "./proforma-document-generation-context.interface"; + +export interface ProformaDocumentGeneratorService< + TContext extends ProformaDocumentGenerationContext = ProformaDocumentGenerationContext, +> extends DocumentGenerationService {} 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 0552a8d5..ce3777bf 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 @@ -2,6 +2,8 @@ import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/ import type { ProformaReportSnapshot } from "../snapshot-builders"; +import type { ProformaDocumentGenerationContext } from "./proforma-document-generation-context.interface"; + /** * Construye los metadatos del documento PDF de una factura emitida. * @@ -10,13 +12,16 @@ import type { ProformaReportSnapshot } from "../snapshot-builders"; * - Sin IO */ export class ProformaDocumentPropertiesFactory - implements IDocumentPropertiesFactory + implements IDocumentPropertiesFactory { - build(snapshot: ProformaReportSnapshot): IDocumentProperties { + build( + snapshot: ProformaReportSnapshot, + context: ProformaDocumentGenerationContext + ): IDocumentProperties { return { title: snapshot.reference ?? snapshot.proforma_reference, subject: "proforma", - author: snapshot.company.legalName, + author: context.companyLegalName, creator: "FactuGES ERP", }; } diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.test.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.test.ts index 96addf38..97eca396 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.test.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/preview-proforma-report.use-case.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { PreviewProformaReportUseCase } from "./preview-proforma-report.use-case"; @@ -41,3 +41,49 @@ test("busca proformas archivadas al generar la vista previa", async () => { assert.equal(activeFinderCalls, 0); assert.equal(includingArchivedFinderCalls, 1); }); + +test("genera la vista previa con el contexto documental mínimo", async () => { + const companyId = UniqueID.generateNewID(); + const proformaId = UniqueID.generateNewID(); + let receivedContext: unknown; + + const useCase = new PreviewProformaReportUseCase({ + finder: { findProformaByIdIncludingArchived: async () => Result.ok({} as never) }, + companyReportProfileFinder: { + findByCompanyId: async () => + Result.ok( + Maybe.some({ + slug: "rodax", + legalName: "Rodax Software", + locale: Maybe.some("es-ES"), + currencyCode: Maybe.some("EUR"), + } as never) + ), + }, + fullReadModelAssembler: { assemble: async () => Result.ok({} as never) }, + fullSnapshotBuilder: { + toOutput: () => ({ language_code: "es", currency_code: "EUR" }) as never, + }, + reportSnapshotBuilder: { toOutput: () => ({}) as never }, + documentService: { + generate: async (_snapshot, context) => { + receivedContext = context; + return Result.ok({ + payload: Buffer.from(""), + filename: "proforma-P-1.html", + mimeType: "text/html", + }); + }, + } as never, + transactionManager: { complete: async (work) => work({} as never) }, + }); + + const result = await useCase.execute({ companyId, proforma_id: proformaId.toString() }); + + assert.equal(result.isSuccess, true); + assert.deepEqual(receivedContext, { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + }); +}); 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 index 11cd3f26..c1cb1e03 100644 --- 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 @@ -91,7 +91,9 @@ export class PreviewProformaReportUseCase { }); const documentResult = await this.deps.documentService.generate(reportSnapshot, { - companySlug: reportSnapshot.company.slug, + companySlug: companyProfileResult.data.unwrap().slug, + languageCode: localization.languageCode, + companyLegalName: companyProfileResult.data.unwrap().legalName, }); if (documentResult.isFailure) { diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.test.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.test.ts index 2c83e354..e894688c 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.test.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma-pdf.use-case.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { ReportProformaPdfUseCase } from "./report-proforma-pdf.use-case"; @@ -41,3 +41,59 @@ test("busca proformas archivadas al generar el PDF", async () => { assert.equal(activeFinderCalls, 0); assert.equal(includingArchivedFinderCalls, 1); }); + +test("genera el PDF con el contexto documental mínimo", async () => { + const companyId = UniqueID.generateNewID(); + const proformaId = UniqueID.generateNewID(); + const reportSnapshot = { + id: proformaId.toString(), + company_id: companyId.toString(), + proforma_reference: "P-1", + } as never; + let receivedContext: unknown; + + const useCase = new ReportProformaPdfUseCase({ + finder: { + findProformaByIdIncludingArchived: async () => Result.ok({} as never), + }, + companyReportProfileFinder: { + findByCompanyId: async () => + Result.ok( + Maybe.some({ + slug: "rodax", + legalName: "Rodax Software", + locale: Maybe.some("es-ES"), + currencyCode: Maybe.some("EUR"), + version: Maybe.some("3"), + } as never) + ), + }, + fullReadModelAssembler: { assemble: async () => Result.ok({} as never) }, + fullSnapshotBuilder: { + toOutput: () => ({ language_code: "es", currency_code: "EUR" }) as never, + }, + reportSnapshotBuilder: { toOutput: () => reportSnapshot }, + documentService: { + generate: async (_snapshot, context) => { + receivedContext = context; + return Result.ok({ + payload: Buffer.from("pdf"), + filename: "proforma-P-1.pdf", + mimeType: "application/pdf", + }); + }, + } as never, + transactionManager: { complete: async (work) => work({} as never) }, + }); + + const result = await useCase.execute({ companyId, proforma_id: proformaId.toString() }); + + assert.equal(result.isSuccess, true); + assert.deepEqual(receivedContext, { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + currencyCode: "EUR", + companyVersion: "3", + }); +}); 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 index 4f2438d7..fad8c596 100644 --- 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 @@ -9,6 +9,7 @@ import type { IProformaFinder, IProformaFullReadModelAssembler, ProformaDocumentGeneratorService, + ProformaPdfDocumentGenerationContext, } from "../services"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report"; @@ -26,7 +27,7 @@ export class ReportProformaPdfUseCase { fullReadModelAssembler: IProformaFullReadModelAssembler; fullSnapshotBuilder: IProformaFullSnapshotBuilder; reportSnapshotBuilder: IProformaReportSnapshotBuilder; - documentService: ProformaDocumentGeneratorService; + documentService: ProformaDocumentGeneratorService; transactionManager: ITransactionManager; } ) {} @@ -93,8 +94,11 @@ export class ReportProformaPdfUseCase { }); const documentResult = await this.deps.documentService.generate(reportSnapshot, { - //localization, - company: companyProfile, + companySlug: companyProfile.slug, + languageCode: localization.languageCode, + companyLegalName: companyProfile.legalName, + currencyCode: localization.currencyCode, + companyVersion: companyProfile.version.getOrUndefined(), }); if (documentResult.isFailure) { 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 index 84766e29..bc29b77c 100644 --- 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 @@ -6,6 +6,7 @@ import { } from "@erp/core/api"; import { + type ProformaDocumentGenerationContext, type ProformaDocumentGeneratorService, ProformaDocumentPropertiesFactory, type ProformaReportSnapshot, @@ -28,13 +29,15 @@ export class ProformaHtmlPreviewPipelineFactory { const propertiesFactory = new ProformaDocumentPropertiesFactory(); const postProcessor = new DocumentPostProcessorChain([]); - return new DocumentGenerationService({ - renderer, - preProcessors: [], - postProcessor, - sideEffects: [], - metadataFactory, - propertiesFactory, - }); + 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 index bcd833d5..05460537 100644 --- 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 @@ -10,6 +10,7 @@ import { import { type ProformaDocumentGeneratorService, ProformaDocumentPropertiesFactory, + type ProformaPdfDocumentGenerationContext, type ProformaReportSnapshot, } from "../../../../application"; import { ProformaPdfCachePreProcessor } from "../pre-processors"; @@ -24,7 +25,9 @@ export interface ProformaPdfDocumentPipelineFactoryDeps { } export class ProformaPdfDocumentPipelineFactory { - static create(deps: ProformaPdfDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService { + static create( + deps: ProformaPdfDocumentPipelineFactoryDeps + ): ProformaDocumentGeneratorService { const preProcessors = [new ProformaPdfCachePreProcessor(deps.documentStorage)]; const renderer = new ProformaPdfDocumentRenderer( deps.fastReportRenderer, @@ -37,7 +40,10 @@ export class ProformaPdfDocumentPipelineFactory { new PersistProformaPdfSideEffect(deps.documentStorage), ]; - return new DocumentGenerationService({ + return new DocumentGenerationService< + ProformaReportSnapshot, + ProformaPdfDocumentGenerationContext + >({ renderer, preProcessors, postProcessor, 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 index bb5a9788..2344eb72 100644 --- 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 @@ -6,18 +6,21 @@ import type { IDocumentRenderer, } from "@erp/core/api"; -import type { ProformaReportSnapshot } from "../../../../../application"; +import type { + ProformaDocumentGenerationContext, + ProformaReportSnapshot, +} from "../../../../../application"; -type ProformaHtmlPreviewRenderParams = { - companySlug: string; - format: string; - languageCode: string; +type ProformaHtmlPreviewRenderParams = ProformaDocumentGenerationContext & { + format: "PDF" | "HTML"; filename: string; mimeType: string; properties: IDocumentProperties; }; -export class ProformaHtmlPreviewRenderer implements IDocumentRenderer { +export class ProformaHtmlPreviewRenderer + implements IDocumentRenderer +{ public constructor( private readonly fastReportRenderer: FastReportRenderer, private readonly templateResolver: FastReportTemplateResolver 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 index ad99d542..a5e17186 100644 --- 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 @@ -6,18 +6,21 @@ import type { IDocumentRenderer, } from "@erp/core/api"; -import type { ProformaReportSnapshot } from "../../../../../application"; +import type { + ProformaPdfDocumentGenerationContext, + ProformaReportSnapshot, +} from "../../../../../application"; -type ProformaPdfDocumentRenderParams = { - companySlug: string; - format: string; - languageCode: string; +type ProformaPdfDocumentRenderParams = ProformaPdfDocumentGenerationContext & { + format: "PDF" | "HTML"; filename: string; mimeType: string; properties: IDocumentProperties; }; -export class ProformaPdfDocumentRenderer implements IDocumentRenderer { +export class ProformaPdfDocumentRenderer + implements IDocumentRenderer +{ public constructor( private readonly fastReportRenderer: FastReportRenderer, private readonly templateResolver: FastReportTemplateResolver diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-document-metadata-factories.test.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-document-metadata-factories.test.ts new file mode 100644 index 00000000..368f4fb1 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/services/proforma-document-metadata-factories.test.ts @@ -0,0 +1,117 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { ProformaDocumentPropertiesFactory } from "../../../../application"; +import { ProformaHtmlPreviewRenderer } from "../renderers/fastreport/proforma-html-preview-renderer"; +import { ProformaPdfDocumentRenderer } from "../renderers/fastreport/proforma-pdf-document-renderer"; + +import { ProformaHtmlPreviewDocumentMetadataFactory } from "./proforma-html-preview-document-metadata-factory"; +import { ProformaPdfDocumentMetadataFactory } from "./proforma-pdf-document-metadata-factory"; + +const snapshot = { + id: "proforma-id", + company_id: "company-id", + proforma_reference: "P-1", + status: "issued", +} as never; + +test("construye metadata PDF desde snapshot y contexto tipado", () => { + const metadata = new ProformaPdfDocumentMetadataFactory().build(snapshot, { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + currencyCode: "EUR", + companyVersion: "3", + }); + + assert.deepEqual(metadata, { + documentType: "proforma", + documentId: "proforma-id", + companyId: "company-id", + companySlug: "rodax", + format: "PDF", + languageCode: "es", + filename: "proforma-P-1.pdf", + storageKey: "proforma:company-id:proforma-id:issued:pdf:es:EUR:rodax:3", + }); +}); + +test("construye metadata HTML sin propiedades inexistentes del snapshot", () => { + const metadata = new ProformaHtmlPreviewDocumentMetadataFactory().build(snapshot, { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + }); + + assert.deepEqual(metadata, { + documentType: "proforma", + documentId: "proforma-id", + companyId: "company-id", + companySlug: "rodax", + format: "HTML", + languageCode: "es", + filename: "proforma-P-1.html", + }); +}); + +test("construye las propiedades desde el contexto sin leer company del snapshot", () => { + const properties = new ProformaDocumentPropertiesFactory().build(snapshot, { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + }); + + assert.deepEqual(properties, { + title: "P-1", + subject: "proforma", + author: "Rodax Software", + creator: "FactuGES ERP", + }); +}); + +test("entrega companySlug y languageCode al resolver la plantilla en ambos renderers", async () => { + const resolvedTemplates: unknown[] = []; + const templateResolver = { + resolveTemplatePath: (params: unknown) => { + resolvedTemplates.push(params); + return "/tmp/proforma.frx"; + }, + }; + const fastReportRenderer = { + render: async () => ({ payload: Buffer.from("document") }), + }; + const renderParams = { + companySlug: "rodax", + languageCode: "es", + companyLegalName: "Rodax Software", + currencyCode: "EUR", + format: "PDF" as const, + filename: "proforma-P-1.pdf", + mimeType: "application/pdf", + properties: {}, + }; + + await new ProformaPdfDocumentRenderer( + fastReportRenderer as never, + templateResolver as never + ).render(snapshot, renderParams); + await new ProformaHtmlPreviewRenderer( + fastReportRenderer as never, + templateResolver as never + ).render(snapshot, renderParams); + + assert.deepEqual(resolvedTemplates, [ + { + module: "customer-invoices", + companySlug: "rodax", + languageCode: "es", + templateFilename: "proforma.frx", + }, + { + module: "customer-invoices", + companySlug: "rodax", + languageCode: "es", + templateFilename: "proforma.frx", + }, + ]); +}); 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 index a3b40b5f..2562a357 100644 --- 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 @@ -1,18 +1,24 @@ import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; -import type { ProformaReportSnapshot } from "../../../../application"; +import type { + ProformaDocumentGenerationContext, + ProformaReportSnapshot, +} from "../../../../application"; export class ProformaHtmlPreviewDocumentMetadataFactory - implements IDocumentMetadataFactory + implements IDocumentMetadataFactory { - build(snapshot: ProformaReportSnapshot): IDocumentMetadata { + build( + snapshot: ProformaReportSnapshot, + context: ProformaDocumentGenerationContext + ): IDocumentMetadata { return { documentType: "proforma", documentId: snapshot.id, companyId: snapshot.company_id, - companySlug: snapshot.company.slug, + companySlug: context.companySlug, format: "HTML", - languageCode: snapshot.localization.languageCode, + languageCode: context.languageCode, filename: `proforma-${snapshot.proforma_reference}.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 index b1675785..e1694a72 100644 --- 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 @@ -1,26 +1,32 @@ import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; -import type { ProformaReportSnapshot } from "../../../../application"; +import type { + ProformaPdfDocumentGenerationContext, + ProformaReportSnapshot, +} from "../../../../application"; export class ProformaPdfDocumentMetadataFactory - implements IDocumentMetadataFactory + implements IDocumentMetadataFactory { - build(snapshot: ProformaReportSnapshot, context: Record): IDocumentMetadata { + build( + snapshot: ProformaReportSnapshot, + context: ProformaPdfDocumentGenerationContext + ): IDocumentMetadata { return { documentType: "proforma", documentId: snapshot.id, - companyId: context.company_id, - companySlug: context.company.slug, + companyId: snapshot.company_id, + companySlug: context.companySlug, format: "PDF", - languageCode: context.localization.languageCode, + languageCode: context.languageCode, filename: `proforma-${snapshot.proforma_reference}.pdf`, - storageKey: this.buildStorageKey(snapshot), + storageKey: this.buildStorageKey(snapshot, context), }; } private buildStorageKey( snapshot: ProformaReportSnapshot, - context: Record + context: ProformaPdfDocumentGenerationContext ): string | undefined { if (snapshot.status === "draft") { return undefined; @@ -35,15 +41,13 @@ export class ProformaPdfDocumentMetadataFactory snapshot.id, snapshot.status, "pdf", - context.localization.languageCode, - context.localization.currencyCode, - context.company.slug, + context.languageCode, + context.currencyCode, + context.companySlug, ]; - const companyVersion = context.company.version.getOrUndefined(); - - if (companyVersion) { - keyParts.push(companyVersion); + if (context.companyVersion) { + keyParts.push(context.companyVersion); } return keyParts.join(":");