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, properties,
}); });
} catch (error) { } 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) // 4. Post-processors (transformaciones)
@ -101,8 +103,9 @@ export class DocumentGenerationService<TSnapshot> {
try { try {
document = await this.postProcessor.process(document, metadata); document = await this.postProcessor.process(document, metadata);
} catch (error) { } catch (error) {
console.error(error); const err = error as Error;
return Result.fail(DocumentGenerationError.postProcess(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 path from "node:path";
import { Renderer } from "../../../../application"; import { Renderer } from "../../../../application";
import { logger } from "../../../logger";
import { FastReportExecutionError, FastReportIOError } from "./fastreport-errors"; import { FastReportExecutionError, FastReportIOError } from "./fastreport-errors";
import type { FastReportExecutableResolver } from "./fastreport-executable-resolver"; import type { FastReportExecutableResolver } from "./fastreport-executable-resolver";
@ -23,34 +24,79 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
} }
async render(options: FastReportRenderOptions): Promise<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 workDir = path.join(os.tmpdir(), "fastreport", randomUUID());
const inputPath = path.join(workDir, "input.json"); const inputPath = path.join(workDir, "input.json");
const outputPath = path.join(workDir, options.format === "PDF" ? "output.pdf" : "output.html"); 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 { try {
await this.ensureWorkDir(workDir);
await writeFile(inputPath, JSON.stringify(options.inputData), "utf-8");
const executablePath = this.executableResolver.resolve(); const executablePath = this.executableResolver.resolve();
await this.processRunner.run(executablePath, { await mkdir(workDir, { recursive: true });
templatePath: options.templatePath, await this.ensureWorkDir(workDir);
data: inputPath,
output: outputPath, await writeFile(inputPath, JSON.stringify(options.inputData), "utf-8");
format: options.format,
properties: options.properties, 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); 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 { return {
payload, payload,
templateChecksum: await this.computeTemplateChecksum(options.templatePath), templateChecksum: checksum,
}; };
} catch (error) { } 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 { } finally {
await this.safeCleanup(workDir); 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 { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
@ -9,7 +9,7 @@ import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report
type ReportProformaUseCaseInput = { type ReportProformaUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
companySlug: string; companySlug: string;
invoice_id: string; proforma_id: string;
format: RendererFormat; format: RendererFormat;
}; };
@ -23,25 +23,29 @@ export class ReportProformaUseCase {
) {} ) {}
public async execute(params: ReportProformaUseCaseInput) { 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) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const invoiceId = idOrError.data; const proformaId = idOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceResult = await this.finder.findProformaById(companyId, invoiceId, transaction); const proformaResult = await this.finder.findProformaById(
companyId,
proformaId,
transaction
);
if (invoiceResult.isFailure) { if (proformaResult.isFailure) {
return Result.fail(invoiceResult.error); return Result.fail(proformaResult.error);
} }
const invoice = invoiceResult.data; const invoice = proformaResult.data;
// Snapshot completo de la entidad // Snapshot completo de la entidad
const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice); const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice);
@ -67,7 +71,9 @@ export class ReportProformaUseCase {
filename: documentResult.data.filename, filename: documentResult.data.filename,
}); });
} catch (error: unknown) { } 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 ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { GetProformaByIdRequestSchema, ListProformasRequestSchema } from "../../../../common"; import {
GetProformaByIdRequestSchema,
ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
} from "../../../../common";
import { import {
GetProformaController, GetProformaController,
ListProformasController, ListProformasController,
type ProformasInternalDeps, type ProformasInternalDeps,
ReportProformaController,
} from "../../proformas"; } from "../../proformas";
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => { 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( /*router.post(
"/", "/",
//checkTabContext, //checkTabContext,
@ -89,19 +108,9 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
const controller = new DeleteProformaController(useCase); const controller = new DeleteProformaController(useCase);
return controller.execute(req, res, next); 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( router.patch(
"/:proforma_id/status", "/:proforma_id/status",

View File

@ -5,8 +5,8 @@ import {
requireAuthenticatedGuard, requireAuthenticatedGuard,
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } 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 type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.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", module: "customer-invoices",
companySlug, companySlug,
languageCode, languageCode,
templateFilename: "issued-invoice.frx", templateFilename: "proforma.frx",
}); });
const output = await this.fastReportRenderer.render({ const output = await this.fastReportRenderer.render({

View File

@ -1,10 +1,12 @@
import { import {
ExpressController, ExpressController,
type RendererFormat,
forbidQueryFieldGuard, forbidQueryFieldGuard,
requireAuthenticatedGuard, requireAuthenticatedGuard,
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common";
import type { ReportProformaUseCase } from "../../../../application/index.ts"; import type { ReportProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.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 { companySlug } = this.getUser();
const { proforma_id } = this.req.params; 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( return result.match(
({ data, filename }) => ({ payload, filename }) => {
filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string), 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) (err) => this.handleError(err)
); );
} }

View File

@ -8,7 +8,12 @@ export type ReportProformaByIdParamsRequestDTO = z.infer<
typeof ReportProformaByIdParamsRequestSchema typeof ReportProformaByIdParamsRequestSchema
>; >;
export const ReportProformaByIdQueryRequestSchema = z.object({ 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< export type ReportProformaByIdQueryRequestDTO = z.infer<

File diff suppressed because one or more lines are too long