This commit is contained in:
David Arranz 2026-02-23 09:49:09 +01:00
parent 056e78548e
commit 821b4d3ff7
9 changed files with 357 additions and 46 deletions

View File

@ -93,7 +93,9 @@ export class DocumentGenerationService<TSnapshot> {
properties,
});
} catch (error) {
return Result.fail(DocumentGenerationError.render(error));
const err = error as Error;
logger.error(err.message, err);
return Result.fail(DocumentGenerationError.render(err));
}
// 4. Post-processors (transformaciones)
@ -101,8 +103,9 @@ export class DocumentGenerationService<TSnapshot> {
try {
document = await this.postProcessor.process(document, metadata);
} catch (error) {
console.error(error);
return Result.fail(DocumentGenerationError.postProcess(error));
const err = error as Error;
logger.error(err.message, err);
return Result.fail(DocumentGenerationError.postProcess(err));
}
}

View File

@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { Renderer } from "../../../../application";
import { logger } from "../../../logger";
import { FastReportExecutionError, FastReportIOError } from "./fastreport-errors";
import type { FastReportExecutableResolver } from "./fastreport-executable-resolver";
@ -23,34 +24,79 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
}
async render(options: FastReportRenderOptions): Promise<FastReportRenderOutput> {
if (!options.templatePath) {
const message = "Option 'templatePath' is required";
logger.error(message, {
label: "FastReportRenderer.render",
options,
});
throw new FastReportExecutionError(message);
}
if (!["PDF", "HTML"].includes(options.format)) {
const message = `Unsupported format: ${options.format}`;
logger.error(message, {
label: "FastReportRenderer.render",
options,
});
throw new FastReportExecutionError(message);
}
const workDir = path.join(os.tmpdir(), "fastreport", randomUUID());
const inputPath = path.join(workDir, "input.json");
const outputPath = path.join(workDir, options.format === "PDF" ? "output.pdf" : "output.html");
await mkdir(workDir, { recursive: true });
const params = {
templatePath: options.templatePath,
data: inputPath,
output: outputPath,
format: options.format,
properties: options.properties,
};
try {
await this.ensureWorkDir(workDir);
await writeFile(inputPath, JSON.stringify(options.inputData), "utf-8");
const executablePath = this.executableResolver.resolve();
await this.processRunner.run(executablePath, {
templatePath: options.templatePath,
data: inputPath,
output: outputPath,
format: options.format,
properties: options.properties,
});
await mkdir(workDir, { recursive: true });
await this.ensureWorkDir(workDir);
await writeFile(inputPath, JSON.stringify(options.inputData), "utf-8");
const result = await this.processRunner.run(executablePath, params);
if (result.isFailure) {
logger.error(result.error.message, {
label: "FastReportRenderer.render",
params,
});
throw new FastReportExecutionError(result.error.message, { cause: result.error });
}
// comprobar salida
await access(outputPath);
const payload = await readFile(outputPath);
if (!payload || payload.length === 0) {
logger.error("Output file is empty", {
label: "FastReportRenderer.render",
params,
});
throw new FastReportExecutionError("Output file is empty");
}
const checksum = await this.computeTemplateChecksum(options.templatePath);
return {
payload,
templateChecksum: await this.computeTemplateChecksum(options.templatePath),
templateChecksum: checksum,
};
} catch (error) {
throw new FastReportExecutionError((error as Error).message);
const err = error as Error;
logger.error(err.message, {
label: "FastReportRenderer.render",
params,
});
throw new FastReportExecutionError(err.message, { cause: err });
} finally {
await this.safeCleanup(workDir);
}

View File

@ -1,4 +1,4 @@
import type { ITransactionManager, RendererFormat } from "@erp/core/api";
import { type ITransactionManager, type RendererFormat, logger } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
@ -9,7 +9,7 @@ import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report
type ReportProformaUseCaseInput = {
companyId: UniqueID;
companySlug: string;
invoice_id: string;
proforma_id: string;
format: RendererFormat;
};
@ -23,25 +23,29 @@ export class ReportProformaUseCase {
) {}
public async execute(params: ReportProformaUseCaseInput) {
const { invoice_id, companyId } = params;
const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id);
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const invoiceId = idOrError.data;
const proformaId = idOrError.data;
return this.transactionManager.complete(async (transaction) => {
try {
const invoiceResult = await this.finder.findProformaById(companyId, invoiceId, transaction);
const proformaResult = await this.finder.findProformaById(
companyId,
proformaId,
transaction
);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const invoice = invoiceResult.data;
const invoice = proformaResult.data;
// Snapshot completo de la entidad
const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice);
@ -67,7 +71,9 @@ export class ReportProformaUseCase {
filename: documentResult.data.filename,
});
} catch (error: unknown) {
return Result.fail(error as Error);
const err = error as Error;
logger.error(err.message, { label: "ReportProformaUseCase.execure" });
return Result.fail(err);
}
});
}

View File

@ -2,11 +2,17 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
import { GetProformaByIdRequestSchema, ListProformasRequestSchema } from "../../../../common";
import {
GetProformaByIdRequestSchema,
ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
} from "../../../../common";
import {
GetProformaController,
ListProformasController,
type ProformasInternalDeps,
ReportProformaController,
} from "../../proformas";
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => {
@ -54,6 +60,19 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
}
);
router.get(
"/:proforma_id/report",
//checkTabContext,
validateRequest(ReportProformaByIdParamsRequestSchema, "params"),
validateRequest(ReportProformaByIdQueryRequestSchema, "query"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.reportProforma();
const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next);
}
);
/*router.post(
"/",
//checkTabContext,
@ -89,19 +108,9 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
const controller = new DeleteProformaController(useCase);
return controller.execute(req, res, next);
}
);
);*/
router.get(
"/:proforma_id/report",
//checkTabContext,
validateRequest(ReportProformaByIdParamsRequestSchema, "params"),
validateRequest(ReportProformaByIdQueryRequestSchema, "query"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.reportIssuedInvoice();
const controller = new ReportProformaController(useCase);
return controller.execute(req, res, next);
}
);
/*
router.patch(
"/:proforma_id/status",

View File

@ -5,8 +5,8 @@ import {
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { ReportIssueInvoiceByIdQueryRequestDTO } from "@erp/customer-invoices/common";
import type { ReportIssueInvoiceByIdQueryRequestDTO } from "../../../../../common";
import type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";

View File

@ -45,7 +45,7 @@ export class ProformaDocumentRenderer implements IDocumentRenderer<ProformaRepor
module: "customer-invoices",
companySlug,
languageCode,
templateFilename: "issued-invoice.frx",
templateFilename: "proforma.frx",
});
const output = await this.fastReportRenderer.render({

View File

@ -1,10 +1,12 @@
import {
ExpressController,
type RendererFormat,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common";
import type { ReportProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
@ -29,13 +31,26 @@ export class ReportProformaController extends ExpressController {
const { companySlug } = this.getUser();
const { proforma_id } = this.req.params;
const { format } = this.req.query as { format: "pdf" | "html" };
const { format } = this.req.query as ReportProformaByIdQueryRequestDTO;
const result = await this.useCase.execute({ proforma_id, companyId, companySlug, format });
const result = await this.useCase.execute({
proforma_id,
companyId,
companySlug,
format: format as RendererFormat,
});
return result.match(
({ data, filename }) =>
filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string),
({ payload, filename }) => {
if (format === "PDF") {
return this.downloadPDF(payload as Buffer<ArrayBuffer>, String(filename));
}
if (format === "HTML") {
return this.downloadHTML(payload as unknown as string);
}
// JSON
return this.json(payload);
},
(err) => this.handleError(err)
);
}

View File

@ -8,7 +8,12 @@ export type ReportProformaByIdParamsRequestDTO = z.infer<
typeof ReportProformaByIdParamsRequestSchema
>;
export const ReportProformaByIdQueryRequestSchema = z.object({
format: z.enum(["pdf", "html"]).default("pdf"),
format: z
.string()
.default("pdf")
.transform((v) => v.trim().toLowerCase())
.pipe(z.enum(["pdf", "html", "json"]))
.transform((v) => v.toUpperCase() as "PDF" | "HTML" | "JSON"),
});
export type ReportProformaByIdQueryRequestDTO = z.infer<

File diff suppressed because one or more lines are too long