v0.5.0
This commit is contained in:
parent
770fb33bb0
commit
aaf7d22374
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "WEB: Vite (Chrome)",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/auth",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/core",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./company-certificate-context.interface";
|
||||
export * from "./document.interface";
|
||||
export * from "./document-metadata.interface";
|
||||
export * from "./document-properties.interface";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<TSnapshot> = {
|
||||
renderer: IDocumentRenderer<TSnapshot>;
|
||||
metadataFactory: IDocumentMetadataFactory<TSnapshot>;
|
||||
propertiesFactory: IDocumentPropertiesFactory<TSnapshot>;
|
||||
preProcessors: readonly IDocumentPreProcessor[];
|
||||
postProcessor: IDocumentPostProcessor;
|
||||
sideEffects: readonly IDocumentSideEffect[];
|
||||
};
|
||||
|
||||
export type DocumentGenerationServiceRenderParams = Record<string, string>;
|
||||
|
||||
export class DocumentGenerationService<TSnapshot> {
|
||||
constructor(
|
||||
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>,
|
||||
private readonly preProcessors: readonly IDocumentPreProcessor[],
|
||||
private readonly renderer: IDocumentRenderer<TSnapshot>,
|
||||
private readonly postProcessor: IDocumentPostProcessor,
|
||||
private readonly sideEffects: readonly IDocumentSideEffect[]
|
||||
) {}
|
||||
private readonly renderer: IDocumentRenderer<TSnapshot>;
|
||||
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>;
|
||||
private readonly propertiesFactory: IDocumentPropertiesFactory<TSnapshot>;
|
||||
private readonly preProcessors: readonly IDocumentPreProcessor[];
|
||||
private readonly postProcessor: IDocumentPostProcessor;
|
||||
private readonly sideEffects: readonly IDocumentSideEffect[];
|
||||
|
||||
constructor(deps: DocumentGenerationServiceDeps<TSnapshot>) {
|
||||
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<Result<IDocument, DocumentGenerationError>> {
|
||||
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<TSnapshot> {
|
||||
languageCode: metadata.languageCode,
|
||||
filename: metadata.filename,
|
||||
mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html",
|
||||
properties,
|
||||
});
|
||||
} catch (error) {
|
||||
return Result.fail(DocumentGenerationError.render(error));
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import type { IDocumentProperties } from "../application-models";
|
||||
|
||||
export interface IDocumentPropertiesFactory<TSnapshot> {
|
||||
build(snapshot: TSnapshot): IDocumentProperties;
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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<Result<Buffer | string, FastReportExecutionError>> {
|
||||
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<string, string | undefined> = {
|
||||
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<Result<Buffer | string, FastReportExecutionError>> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(executablePath, args, {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -40,6 +40,7 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
|
||||
data: inputPath,
|
||||
output: outputPath,
|
||||
format: options.format,
|
||||
properties: options.properties,
|
||||
});
|
||||
|
||||
const payload = await readFile(outputPath);
|
||||
@ -82,98 +83,3 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
protected async renderInternal(
|
||||
options: FastReportRenderOptions
|
||||
): Promise<FastReportRenderOutput> {
|
||||
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<Maybe<Buffer | string>> {
|
||||
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<Result<Buffer | string, FastReportError>> {
|
||||
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<void> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@ -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) {
|
||||
// <root>/<module>/templates/<companySlug>/
|
||||
return this.resolveJoin([module, "templates", companySlug]);
|
||||
// <root>/<module>/templates/<companySlug>/<languageCode>/
|
||||
return this.resolveJoin([module, "templates", companySlug, languageCode]);
|
||||
}
|
||||
|
||||
// <root>/templates/<module>/<companySlug>/
|
||||
//return this.resolveJoin(["templates", module, companySlug]);
|
||||
return this.resolveJoin([module]);
|
||||
// <root>/templates/<companySlug>/<module>/<languageCode>
|
||||
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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<string, string>();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./handlebars-template-resolver";
|
||||
@ -1,3 +1 @@
|
||||
export * from "./fastreport";
|
||||
export * from "./handlebars";
|
||||
export * from "./renderer-template-resolver-SOBRA";
|
||||
|
||||
@ -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) {
|
||||
// <root>/<module>/templates/<companySlug>/
|
||||
return this.resolveJoin([module, "templates", companySlug]);
|
||||
}
|
||||
|
||||
// <root>/templates/<module>/<companySlug>/
|
||||
//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");
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customer-invoices",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<IssuedInvoiceReportSnapshot>
|
||||
{
|
||||
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentProperties {
|
||||
return {
|
||||
title: snapshot.reference,
|
||||
subject: "issued-invoice",
|
||||
author: snapshot.company_slug,
|
||||
creator: "FactuGES ERP",
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,6 @@ export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => {
|
||||
documentSigningService: documentSigning.signingService,
|
||||
|
||||
//
|
||||
documentCacheStore: documentStorage.cacheStore,
|
||||
documentStorage: documentStorage.storage,
|
||||
|
||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
||||
|
||||
@ -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<IssuedInvoiceReportSnapshot>(
|
||||
metadataFactory,
|
||||
return new DocumentGenerationService<IssuedInvoiceReportSnapshot>({
|
||||
renderer,
|
||||
preProcessors,
|
||||
documentRenderer,
|
||||
postProcessor,
|
||||
sideEffects
|
||||
);
|
||||
sideEffects,
|
||||
metadataFactory,
|
||||
propertiesFactory,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<IDocument> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/doc-numbering",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-criteria",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-ddd",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-logger",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-utils",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>1.8.2.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.9.0.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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 \
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user