diff --git a/.vscode/launch.json b/.vscode/launch.json index 9a99b3c0..b0862ac3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,5 +1,5 @@ { - "version": "0.4.8", + "version": "0.5.0", "configurations": [ { "name": "WEB: Vite (Chrome)", diff --git a/apps/server/package.json b/apps/server/package.json index 2067ccc0..d47bdf51 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges-server", - "version": "0.4.8", + "version": "0.5.0", "private": true, "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", diff --git a/apps/web/package.json b/apps/web/package.json index f7895073..3ab16ce7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@erp/factuges-web", "private": true, - "version": "0.4.8", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite --host --clearScreen false", diff --git a/modules/auth/package.json b/modules/auth/package.json index 7898f699..228301e9 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@erp/auth", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/package.json b/modules/core/package.json index 2bacc603..04f7afcf 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/src/api/application/documents/application-models/document-properties.interface.ts b/modules/core/src/api/application/documents/application-models/document-properties.interface.ts new file mode 100644 index 00000000..a8f7815c --- /dev/null +++ b/modules/core/src/api/application/documents/application-models/document-properties.interface.ts @@ -0,0 +1,8 @@ +export interface IDocumentProperties { + readonly title?: string; + readonly author?: string; + readonly subject?: string; + readonly keywords?: string; + readonly creator?: string; + readonly producer?: string; +} diff --git a/modules/core/src/api/application/documents/application-models/index.ts b/modules/core/src/api/application/documents/application-models/index.ts index 134f1282..abdc294e 100644 --- a/modules/core/src/api/application/documents/application-models/index.ts +++ b/modules/core/src/api/application/documents/application-models/index.ts @@ -1,3 +1,4 @@ export * from "./company-certificate-context.interface"; export * from "./document.interface"; export * from "./document-metadata.interface"; +export * from "./document-properties.interface"; diff --git a/modules/core/src/api/application/documents/errors/document-generation-error.ts b/modules/core/src/api/application/documents/errors/document-generation-error.ts index 59b5a3d0..c5f22ae5 100644 --- a/modules/core/src/api/application/documents/errors/document-generation-error.ts +++ b/modules/core/src/api/application/documents/errors/document-generation-error.ts @@ -2,6 +2,7 @@ import { ApplicationError } from "@repo/rdx-ddd"; type DocumentGenerationErrorCode = | "METADATA_ERROR" + | "PROPERTIES_ERROR" | "CACHE_ERROR" // no fatal (solo para métricas/logs si se usa) | "RENDER_ERROR" | "POST_PROCESS_ERROR" @@ -30,6 +31,10 @@ export class DocumentGenerationError extends ApplicationError { return new DocumentGenerationError("Invalid document metadata", "METADATA_ERROR", cause); } + static properties(cause: unknown) { + return new DocumentGenerationError("Invalid document properties", "PROPERTIES_ERROR", cause); + } + static render(cause: unknown) { return new DocumentGenerationError("Document render failed", "RENDER_ERROR", cause); } 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 b0438ae2..551fdcb4 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 @@ -1,7 +1,7 @@ -import { logger } from "@erp/core/api"; +import { type IDocumentPropertiesFactory, logger } from "@erp/core/api"; import { Result } from "@repo/rdx-utils"; -import type { IDocument, IDocumentMetadata } from "../application-models"; +import type { IDocument, IDocumentMetadata, IDocumentProperties } from "../application-models"; import { DocumentGenerationError } from "../errors"; import type { IDocumentMetadataFactory } from "./document-metadata-factory.interface"; @@ -21,30 +21,54 @@ 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; + preProcessors: readonly IDocumentPreProcessor[]; + postProcessor: IDocumentPostProcessor; + sideEffects: readonly IDocumentSideEffect[]; +}; + export type DocumentGenerationServiceRenderParams = Record; export class DocumentGenerationService { - constructor( - private readonly metadataFactory: IDocumentMetadataFactory, - private readonly preProcessors: readonly IDocumentPreProcessor[], - private readonly renderer: IDocumentRenderer, - private readonly postProcessor: IDocumentPostProcessor, - private readonly sideEffects: readonly IDocumentSideEffect[] - ) {} + private readonly renderer: IDocumentRenderer; + 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) { + this.renderer = deps.renderer; + this.metadataFactory = deps.metadataFactory; + this.propertiesFactory = deps.propertiesFactory; + this.preProcessors = deps.preProcessors; + this.postProcessor = deps.postProcessor; + this.sideEffects = deps.sideEffects; + } async generate( snapshot: TSnapshot, params: DocumentGenerationServiceRenderParams ): Promise> { let metadata: IDocumentMetadata; + let properties: IDocumentProperties; - // 1. Metadata + // 1. Metadata and Properties try { metadata = this.metadataFactory.build(snapshot); } catch (error) { return Result.fail(DocumentGenerationError.metadata(error)); } + try { + properties = this.propertiesFactory.build(snapshot); + } catch (error) { + return Result.fail(DocumentGenerationError.properties(error)); + } + // 2. Pre-processors (cache / short-circuit) for (const preProcessor of this.preProcessors) { try { @@ -66,6 +90,7 @@ export class DocumentGenerationService { languageCode: metadata.languageCode, filename: metadata.filename, mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html", + properties, }); } catch (error) { return Result.fail(DocumentGenerationError.render(error)); 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 new file mode 100644 index 00000000..9917511c --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-properties-factory.interface.ts @@ -0,0 +1,5 @@ +import type { IDocumentProperties } from "../application-models"; + +export interface IDocumentPropertiesFactory { + build(snapshot: TSnapshot): IDocumentProperties; +} diff --git a/modules/core/src/api/application/documents/services/index.ts b/modules/core/src/api/application/documents/services/index.ts index da602266..f9d42d7f 100644 --- a/modules/core/src/api/application/documents/services/index.ts +++ b/modules/core/src/api/application/documents/services/index.ts @@ -4,6 +4,7 @@ export * from "./document-metadata-factory.interface"; export * from "./document-post-processor.interface"; export * from "./document-post-processor-chain"; export * from "./document-pre-processor.interface"; +export * from "./document-properties-factory.interface"; export * from "./document-renderer.interface"; export * from "./document-side-effect.interface"; export * from "./document-signing-service.interface"; diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts index 3b36e0bd..a0d0e399 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import { Result } from "@repo/rdx-utils"; import { FastReportExecutionError } from "./fastreport-errors"; +import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type"; /** * Ejecuta el binario FastReport como proceso externo. @@ -17,8 +18,9 @@ import { FastReportExecutionError } from "./fastreport-errors"; export type FastReportProcessRunnerArgs = { templatePath: string; // Path to FRX template (required) data: string; // JSON data as string - format: "PDF" | "HTML"; + format: string; output: string; // Directorio de trabajo temporal + properties: FastReportRenderReportProperties; }; export class FastReportProcessRunner { @@ -26,25 +28,14 @@ export class FastReportProcessRunner { executablePath: string, executableArgs: FastReportProcessRunnerArgs ): Promise> { - const { templatePath, data, format, output } = executableArgs; - - // Guardar datos de entrada en JSON - //const dataPath = buildSafePath({ basePath: workdir, segments: [], filename: "data.json" }); - - // Path de output según formato y con - /*const outputPath = buildSafePath({ - basePath: workdir, - segments: [], - filename: format === "PDF" ? "output.pdf" : "output.html", - }); - - await fs.writeFile(dataPath, data, "utf-8");*/ + const { templatePath, data, format, output, properties } = executableArgs; const args = this.buildArgs({ templatePath, data, output, format, + properties, }); return this.executeProcess(executablePath, args, output, executableArgs.format); @@ -54,21 +45,41 @@ export class FastReportProcessRunner { templatePath: string; data: string; output: string; - format: "PDF" | "HTML"; + format: string; + properties: FastReportRenderReportProperties; }): string[] { - return [ - `--template=${params.templatePath}`, - `--data=${params.data}`, - `--output=${params.output}`, - `--format=${params.format}`, + const { templatePath, data, output, format, properties } = params; + + const args: string[] = [ + `--template=${templatePath}`, + `--data=${data}`, + `--output=${output}`, + `--format=${format}`, ]; + + const optionalArgs: Record = { + title: properties.title, + author: properties.author, + subject: properties.subject, + keywords: properties.keywords, + creator: properties.creator, + producer: properties.producer, + }; + + for (const [key, value] of Object.entries(optionalArgs)) { + if (value !== undefined) { + args.push(`--${key}=${value}`); + } + } + + return args; } private executeProcess( executablePath: string, args: string[], outputPath: string, - format: "PDF" | "HTML" + format: string ): Promise> { return new Promise((resolve) => { const child = spawn(executablePath, args, { diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts index 36b59bc0..94462719 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts @@ -1,6 +1,9 @@ +import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type"; + export type FastReportRenderOptions = { templatePath: string; inputData: unknown; - format: "PDF" | "HTML"; + format: string; storageKey?: string; + properties: FastReportRenderReportProperties; }; diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-report-properties.type.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-report-properties.type.ts new file mode 100644 index 00000000..22e9ffe5 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-report-properties.type.ts @@ -0,0 +1,8 @@ +export type FastReportRenderReportProperties = { + readonly title?: string; + readonly author?: string; + readonly subject?: string; + readonly keywords?: string; + readonly creator?: string; + readonly producer?: string; +}; diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts index 3789b35f..83a890a3 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts @@ -40,6 +40,7 @@ export class FastReportRenderer extends Renderer { - if (process.env.NODE_ENV !== "development") { - // Cache (read-through) - const cached = await this.tryReadFromCache(options.storageKey); - if (cached.isSome()) { - return { - payload: cached.unwrap(), - templateChecksum: "CACHED", - }; - } - } - - // Resolver plantilla - const templatePath = this.resolveTemplatePath(); - console.log("Using FastReport template:", templatePath); - - if (!fs.existsSync(templatePath)) { - throw new FastReportTemplateNotFoundError(templatePath); - } - - const templateChecksum = this.calculateChecksum(templatePath); - - // Llamar a FastReport - const callResult = await this.callFastReportGenerator(options, templatePath); - - if (callResult.isFailure) { - throw callResult.error; - } - - // Guardar documento generado (best-effort) - await this.storageReport(options.storageKey, callResult.data); - - return { - payload: callResult.data, - templateChecksum, - }; - } - - protected async tryReadFromCache(docKey: ReportStorageKey): Promise> { - if (await this.reportStorage.exists(docKey)) { - const cached = await this.reportStorage.read(docKey); - return Maybe.some(cached); - } - return Maybe.none(); - } - - protected async callFastReportGenerator( - options: FastReportRenderOptions, - templatePath: string - ): Promise> { - const executablePath = this.executableResolver.resolve(); - const workdir = this.resolveWorkdir(); - - const runResult = await this.processRunner.run(executablePath, { - templatePath, - data: JSON.stringify(options.inputData), - format: options.format, - workdir, - }); - - if (runResult.isFailure) { - return Result.fail(new FastReportExecutionError(runResult.error.message, runResult.error)); - } - - return Result.ok(runResult.data); - } - - protected async storageReport(key: ReportStorageKey, payload: Buffer | string): Promise { - try { - await this.reportStorage.write(key, payload); - } catch (error) { - // ⚠️ Importante: no romper generación por fallo de cache - } - } - - protected resolveWorkdir(): string { - return fs.mkdtempSync(path.join(os.tmpdir(), "fastreport-")); - } - - -protected abstract resolveTemplatePath(): string; - -protected calculateChecksum(filePath: string): string -{ - const buffer = fs.readFileSync(filePath); - return crypto.createHash("sha256").update(buffer).digest("hex"); -} -} - -*/ diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-template-resolver.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-template-resolver.ts index b8c663ad..eaf84023 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-template-resolver.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-template-resolver.ts @@ -18,17 +18,21 @@ export class FastReportTemplateResolver { * Devuelve el directorio donde residen las plantillas de un módulo/empresa * según el entorno (dev/prod). */ - protected resolveTemplateDirectory(module: string, companySlug: string): string { + protected resolveTemplateDirectory(params: { + module: string; + companySlug: string; + languageCode: string; + }): string { + const { module, companySlug, languageCode } = params; const isDev = process.env.NODE_ENV === "development"; if (isDev) { - // //templates// - return this.resolveJoin([module, "templates", companySlug]); + // //templates/// + return this.resolveJoin([module, "templates", companySlug, languageCode]); } - // /templates/// - //return this.resolveJoin(["templates", module, companySlug]); - return this.resolveJoin([module]); + // /templates/// + return this.resolveJoin([companySlug, module, languageCode]); } /** Resuelve una ruta de recurso relativa al directorio de plantilla */ @@ -39,13 +43,19 @@ export class FastReportTemplateResolver { /** * Devuelve la ruta absoluta del fichero de plantilla. */ - public resolveTemplatePath(module: string, companySlug: string, templateName: string): string { - const dir = this.resolveTemplateDirectory(module, companySlug); - const filePath = this.resolveAssetPath(dir, templateName); + public resolveTemplatePath(params: { + module: string; + companySlug: string; + languageCode: string; + templateFilename: string; + }): string { + const { module, companySlug, languageCode, templateFilename } = params; + const dir = this.resolveTemplateDirectory({ module, companySlug, languageCode }); + const filePath = this.resolveAssetPath(dir, templateFilename); if (!existsSync(filePath)) { throw new FastReportTemplateNotFoundError( - `Template not found: module=${module} company=${companySlug} name=${templateName}` + `Template not found: module=${module} company=${companySlug} name=${templateFilename}` ); } diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts index 87a63ee7..f6848aad 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts @@ -2,5 +2,6 @@ export * from "./fastreport-errors"; export * from "./fastreport-executable-resolver"; export * from "./fastreport-process-runner"; export * from "./fastreport-render-options.type"; +export * from "./fastreport-render-report-properties.type"; export * from "./fastreport-renderer"; export * from "./fastreport-template-resolver"; diff --git a/modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts b/modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts deleted file mode 100644 index f80a2765..00000000 --- a/modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; - -import Handlebars from "handlebars"; -import { lookup } from "mime-types"; - -import { RendererTemplateResolver } from "../renderer-template-resolver-SOBRA"; - -export class HandlebarsTemplateResolver extends RendererTemplateResolver { - protected readonly hbs = Handlebars.create(); - protected registered = false; - protected readonly assetCache = new Map(); - - /** - * Registra el helper "asset". - * - * - Si el fichero termina en .b64 → se asume que el contenido ya es base64 - * - Si no → se lee binario y se convierte a base64 - */ - protected registerAssetHelper(templateDir: string) { - // Si ya está registrado, no hacer nada - if (this.registered) return; - - this.hbs.registerHelper("asset", (resource: string) => { - const assetPath = this.resolveAssetPath(templateDir, resource); - const cacheKey = `${assetPath}`; - - // 1) Caché en memoria - const cached = this.assetCache.get(cacheKey); - if (cached) { - return cached; - } - - if (!existsSync(assetPath)) { - throw new Error(`Asset not found: ${assetPath}`); - } - - // 3) Modo "base64" - const isPreencoded = assetPath.endsWith(".b64"); - - let base64: string; - let mimeType: string; - let value: string; - - if (isPreencoded) { - // Fichero ya contiene el base64 en texto plano - base64 = readFileSync(assetPath, "utf8").trim(); - - // Para el MIME usamos el nombre "original" sin .b64 - const mimeLookupPath = assetPath.replace(/\.b64$/, ""); - mimeType = (lookup(mimeLookupPath) || "application/octet-stream") as string; - value = `data:${mimeType};base64,${base64}`; - } else { - // Fichero normal - - // Si es un CSS no se convierte y se incrusta - const isCSS = assetPath.endsWith(".css"); - if (isCSS) { - const buffer = readFileSync(assetPath); - value = buffer.toString(); - } else { - // En otro caso, se transforma a Base64 - const buffer = readFileSync(assetPath); - mimeType = (lookup(assetPath) || "application/octet-stream") as string; - base64 = buffer.toString("base64"); - value = `data:${mimeType};base64,${base64}`; - } - } - - this.assetCache.set(cacheKey, value); - return value; - }); - - this.registered = true; - } - - /** Compilación directa desde string (sin resolución de rutas) */ - public compile(templateSource: string) { - return this.hbs.compile(templateSource); - } - - /** Localiza → lee → registra helpers → compila */ - public compileTemplate( - module: string, - companySlug: string, - templateName: string - ): Handlebars.TemplateDelegate { - // 1) Directorio de plantillas - const templateDir = this.resolveTemplateDirectory(module, companySlug); - const templatePath = this.resolveTemplatePath(module, companySlug, templateName); // 2) Path completo del template - const source = this.readTemplateFile(templatePath); // Contenido - - this.registerAssetHelper(templateDir); - - // 5) Compilar - return this.compile(source); - } -} diff --git a/modules/core/src/api/infrastructure/documents/renderers/handlebars/index.ts b/modules/core/src/api/infrastructure/documents/renderers/handlebars/index.ts deleted file mode 100644 index 1ae3840e..00000000 --- a/modules/core/src/api/infrastructure/documents/renderers/handlebars/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./handlebars-template-resolver"; diff --git a/modules/core/src/api/infrastructure/documents/renderers/index.ts b/modules/core/src/api/infrastructure/documents/renderers/index.ts index ecdf24e4..3d635d3a 100644 --- a/modules/core/src/api/infrastructure/documents/renderers/index.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/index.ts @@ -1,3 +1 @@ export * from "./fastreport"; -export * from "./handlebars"; -export * from "./renderer-template-resolver-SOBRA"; diff --git a/modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts b/modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts deleted file mode 100644 index f8e1b4d6..00000000 --- a/modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -import type { IRendererTemplateResolver } from "../../../application"; - -import { FastReportTemplateNotFoundError } from "./fastreport"; - -/** - * Resuelve rutas de plantillas para desarrollo y producción. - */ -export abstract class RendererTemplateResolver implements IRendererTemplateResolver { - constructor(protected readonly rootPath: string) {} - - /** Une partes de ruta relativas al rootPath */ - protected resolveJoin(parts: string[]): string { - return join(this.rootPath, ...parts); - } - - /** - * Devuelve el directorio donde residen las plantillas de un módulo/empresa - * según el entorno (dev/prod). - */ - protected resolveTemplateDirectory(module: string, companySlug: string): string { - const isDev = process.env.NODE_ENV === "development"; - - if (isDev) { - // //templates// - return this.resolveJoin([module, "templates", companySlug]); - } - - // /templates/// - //return this.resolveJoin(["templates", module, companySlug]); - return this.resolveJoin([module]); - } - - /** Resuelve una ruta de recurso relativa al directorio de plantilla */ - protected resolveAssetPath(templateDir: string, relative: string): string { - return join(templateDir, relative); - } - - /** - * Devuelve la ruta absoluta del fichero de plantilla. - */ - public resolveTemplatePath(module: string, companySlug: string, templateName: string): string { - const dir = this.resolveTemplateDirectory(module, companySlug); - const filePath = this.resolveAssetPath(dir, templateName); - - if (!existsSync(filePath)) { - throw new FastReportTemplateNotFoundError( - `Template not found: module=${module} company=${companySlug} name=${templateName}` - ); - } - - return filePath; - } - - /** Lee el contenido de un fichero plantilla */ - protected readTemplateFile(templatePath: string): string { - return readFileSync(templatePath, "utf8"); - } -} diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index e54305b5..745ac74d 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts index 15040b53..ea1cbc4e 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts @@ -1,4 +1,5 @@ export * from "./issued-invoice-document-generator.interface"; export * from "./issued-invoice-document-metadata-factory"; +export * from "./issued-invoice-document-properties-factory"; export * from "./issued-invoice-document-renderer.interface"; export * from "./issued-invoice-finder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-properties-factory.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-properties-factory.ts new file mode 100644 index 00000000..bcfecb56 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-properties-factory.ts @@ -0,0 +1,23 @@ +import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api"; + +import type { IssuedInvoiceReportSnapshot } from "../application-models"; + +/** + * Construye los metadatos del documento PDF de una factura emitida. + * + * - Application-level + * - Determinista + * - Sin IO + */ +export class IssuedInvoiceDocumentPropertiesFactory + implements IDocumentPropertiesFactory +{ + build(snapshot: IssuedInvoiceReportSnapshot): IDocumentProperties { + return { + title: snapshot.reference, + subject: "issued-invoice", + author: snapshot.company_slug, + creator: "FactuGES ERP", + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts index b13ee446..857f7118 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts @@ -16,7 +16,6 @@ export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => { documentSigningService: documentSigning.signingService, // - documentCacheStore: documentStorage.cacheStore, documentStorage: documentStorage.storage, templateResolver: documentRenderers.fastReportTemplateResolver, diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts index 3d6272d3..956c28d8 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts @@ -13,6 +13,7 @@ import { import { type IssuedInvoiceDocumentGeneratorService, IssuedInvoiceDocumentMetadataFactory, + IssuedInvoiceDocumentPropertiesFactory, type IssuedInvoiceReportSnapshot, } from "../../../application"; import { DigitalSignaturePostProcessor } from "../post-processors"; @@ -44,13 +45,14 @@ export class IssuedInvoiceDocumentPipelineFactory { const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)]; // 2. Renderer (FastReport) - const documentRenderer = new IssuedInvoiceDocumentRenderer( + const renderer = new IssuedInvoiceDocumentRenderer( deps.fastReportRenderer, deps.templateResolver ); - // 3) Metadata factory (Application) + // 3) Metadata and properties factory (Application) const metadataFactory = new IssuedInvoiceDocumentMetadataFactory(); + const propertiesFactory = new IssuedInvoiceDocumentPropertiesFactory(); // 3) Firma real (Core / Infra) const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([ @@ -63,12 +65,13 @@ export class IssuedInvoiceDocumentPipelineFactory { ]; // 5. Pipeline final - return new DocumentGenerationService( - metadataFactory, + return new DocumentGenerationService({ + renderer, preProcessors, - documentRenderer, postProcessor, - sideEffects - ); + sideEffects, + metadataFactory, + propertiesFactory, + }); } } diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts index d678e3c1..f6f0d344 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts @@ -2,6 +2,7 @@ import type { FastReportRenderer, FastReportTemplateResolver, IDocument, + IDocumentProperties, IDocumentRenderer, } from "@erp/core/api"; @@ -20,6 +21,11 @@ import type { IssuedInvoiceReportSnapshot } from "../../../../application"; export type IssuedInvoiceDocumentRenderParams = { companySlug: string; + format: string; + languageCode: string; + filename: string; + mimeType: string; + properties: IDocumentProperties; }; export class IssuedInvoiceDocumentRenderer @@ -34,23 +40,27 @@ export class IssuedInvoiceDocumentRenderer snapshot: IssuedInvoiceReportSnapshot, params: IssuedInvoiceDocumentRenderParams ): Promise { - const { companySlug } = params; - const templatePath = this.templateResolver.resolveTemplatePath( - "customer-invoices", + const { companySlug, format, languageCode, filename, mimeType, properties } = params; + + // Template + const templatePath = this.templateResolver.resolveTemplatePath({ + module: "customer-invoices", companySlug, - "issued-invoice.frx" - ); + languageCode, + templateFilename: "issued-invoice.frx", + }); const output = await this.fastReportRenderer.render({ templatePath, inputData: snapshot, - format: "PDF", + format, + properties, }); return { payload: this.normalizePayload(output.payload), - mimeType: "application/pdf", - filename: "issued-invoice.pdf", + mimeType, + filename, }; } diff --git a/modules/customer-invoices/templates/rodax/issued-invoice.frx b/modules/customer-invoices/templates/rodax/es/issued-invoice.frx similarity index 100% rename from modules/customer-invoices/templates/rodax/issued-invoice.frx rename to modules/customer-invoices/templates/rodax/es/issued-invoice.frx diff --git a/modules/customers/package.json b/modules/customers/package.json index c22f81b8..8bf4d1e0 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customers", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json index be3e618a..0f88a058 100644 --- a/modules/doc-numbering/package.json +++ b/modules/doc-numbering/package.json @@ -1,6 +1,6 @@ { "name": "@erp/doc-numbering", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index fc60b078..fc2e1fb6 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-criteria", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index 50ed6a2d..8b1239cc 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ddd", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index 8df5484e..07cd86ef 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-logger", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index 75485a5d..53ac562a 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-utils", - "version": "0.4.8", + "version": "0.5.0", "private": true, "type": "module", "sideEffects": false, diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/FastReportCliGenerator.csproj b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/FastReportCliGenerator.csproj index 2f194ea3..1af8b640 100755 --- a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/FastReportCliGenerator.csproj +++ b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/FastReportCliGenerator.csproj @@ -11,7 +11,7 @@ - 1.8.2.0 + 1.9.0.0 diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/Program.cs b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/Program.cs index 566420a9..af719180 100755 --- a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/Program.cs +++ b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/Program.cs @@ -28,7 +28,7 @@ namespace NetCore8._0 return 0; } - // Validación de obligatorios + // Validación de argumentos obligatorios if (!options.TryGetValue("template", out var frxPath) || !options.TryGetValue("data", out var jsonPath) || !options.TryGetValue("output", out var outputPath)) @@ -38,6 +38,15 @@ namespace NetCore8._0 return 1; } + // Argumentos opcionales + options.TryGetValue("title", out var title); + options.TryGetValue("author", out var author); + options.TryGetValue("subject", out var subject); + options.TryGetValue("keywords", out var keywords); + options.TryGetValue("creator", out var creator); + options.TryGetValue("producer", out var producer); + + if (!File.Exists(jsonPath)) throw new FileNotFoundException($"Data file not found: {jsonPath}"); @@ -105,12 +114,13 @@ namespace NetCore8._0 pdfExport.PdfCompliance = PDFExport.PdfStandard.PdfX_3; pdfExport.ShowProgress = false; - pdfExport.Subject = "Generated by FastReportCliGenerator"; - pdfExport.Author = "FastReport .NET"; - pdfExport.Title = "FastReport CLI Report"; - pdfExport.Keywords = ""; - pdfExport.Creator = GetVersion(); - pdfExport.Producer = GetVersion(); + // Metadatos dinámicos + pdfExport.Title = title ?? ""; + pdfExport.Author = author ?? ""; + pdfExport.Subject = subject ?? ""; + pdfExport.Keywords = keywords ?? ""; + pdfExport.Creator = creator ?? GetVersion(); + pdfExport.Producer = producer ?? GetVersion(); report.Export(pdfExport, fs); Console.WriteLine($"Generated PDF: {outputPath}"); @@ -190,14 +200,27 @@ Options: --data Path to JSON data file (required) --output Output file path (required) --format html or pdf (default) + + --title PDF document title (document id or number) + --author PDF document author (company) + --subject PDF document subject (document type) + --keywords PDF document keywords + --creator PDF document creator (application name) + --producer PDF document producer (version) + --version Show version and exit --help Show this help Examples: FastReportCliGenerator \ - --template=invoice.frx \ - --data=invoice.json \ - --output=invoice.html + --template=factura.frx \ + --data=factura.json \ + --output=factura.pdf \ + --format=pdf \ + --title='Factura 2025-001' \ + --author='FactuGES ERP' \ + --subject='Factura cliente ACME' \ + --keywords='factura,cliente,acme' FastReportCliGenerator \ --template=invoice.frx \ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator index c3b06f99..c79ce77f 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe index 929d5f85..2cbf7f63 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe differ