diff --git a/.vscode/settings.json b/.vscode/settings.json index 7bf70877..735080f1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,7 +52,7 @@ // other vscode settings "[handlebars]": { - "editor.defaultFormatter": "vscode.html-language-features" + "editor.defaultFormatter": "mfeckies.handlebars-formatter" }, "[sql]": { "editor.defaultFormatter": "cweijan.vscode-mysql-client2" diff --git a/biome.json b/biome.json index 6501d228..548a61c5 100644 --- a/biome.json +++ b/biome.json @@ -292,6 +292,14 @@ "enabled": true } }, + "html": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + } + }, "overrides": [ { "includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"], diff --git a/modules/core/src/api/application/presenters/presenter-registry.interface.ts b/modules/core/src/api/application/presenters/presenter-registry.interface.ts index 81c5e344..2a1ec137 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.interface.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.interface.ts @@ -1,4 +1,4 @@ -import { IPresenter } from "./presenter.interface"; +import type { IPresenter } from "./presenter.interface"; /** * 🔑 Claves de proyección comunes para seleccionar presenters diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index 6041e22b..ea5463f8 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -1,10 +1,11 @@ -import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { NextFunction, Request, Response } from "express"; +import { type Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import type { NextFunction, Request, Response } from "express"; import httpStatus from "http-status"; -import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper"; + +import { type ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper"; import { - ApiError, + type ApiError, ConflictApiError, ForbiddenApiError, InternalApiError, @@ -13,7 +14,7 @@ import { UnavailableApiError, ValidationApiError, } from "./errors"; -import { GuardFn } from "./express-guards"; +import type { GuardFn } from "./express-guards"; export abstract class ExpressController { protected req!: Request; @@ -125,6 +126,15 @@ export abstract class ExpressController { return this.res.send(pdfBuffer); } + public downloadHTML(htmlString: string) { + this.res.set({ + "Content-Type": "text/html; charset=utf-8", + "Content-Length": Buffer.byteLength(htmlString, "utf-8"), + }); + + return this.res.send(htmlString); + } + protected clientError(message: string, errors?: any[] | any) { return this.handleApiError( new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]) diff --git a/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts b/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts index 87485a56..a2a52301 100644 --- a/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts +++ b/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts @@ -5,11 +5,6 @@ import { lookup } from "mime-types"; import { TemplateResolver } from "./template-resolver"; -interface AssetHelperOptions { - baseDir: string; - mode: "local_file" | "base64"; -} - export class HandlebarsTemplateResolver extends TemplateResolver { protected readonly hbs = Handlebars.create(); protected registered = false; @@ -18,18 +13,16 @@ export class HandlebarsTemplateResolver extends TemplateResolver { /** * Registra el helper "asset". * - * - Si `mode === "local_file"` → devuelve file://... - * - Si `mode === "base64"`: * - 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, mode: "local_file" | "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 = `${mode}:${assetPath}`; + const cacheKey = `${assetPath}`; // 1) Caché en memoria const cached = this.assetCache.get(cacheKey); @@ -41,18 +34,12 @@ export class HandlebarsTemplateResolver extends TemplateResolver { throw new Error(`Asset not found: ${assetPath}`); } - // 2) Modo "local_file": solo devolver la ruta de fichero - if (mode === "local_file") { - const value = `file://${assetPath.replace(/\\/g, "/")}`; - this.assetCache.set(cacheKey, value); - return value; - } - // 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 @@ -61,14 +48,24 @@ export class HandlebarsTemplateResolver extends TemplateResolver { // 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 binario normal → convertimos a base64 - const buffer = readFileSync(assetPath); - mimeType = (lookup(assetPath) || "application/octet-stream") as string; - base64 = buffer.toString("base64"); + // 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}`; + } } - const value = `data:${mimeType};base64,${base64}`; this.assetCache.set(cacheKey, value); return value; }); @@ -87,14 +84,12 @@ export class HandlebarsTemplateResolver extends TemplateResolver { companySlug: string, templateName: string ): Handlebars.TemplateDelegate { - const isDev = process.env.NODE_ENV === "development"; - // 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, isDev ? "local_file" : "base64"); + this.registerAssetHelper(templateDir); // 5) Compilar return this.compile(source); diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 73572492..3018217d 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -55,8 +55,7 @@ "libphonenumber-js": "^1.12.7", "lucide-react": "^0.503.0", "pg-hstore": "^2.3.4", - "puppeteer": "^24.20.0", - "puppeteer-report": "^3.2.0", + "puppeteer": "^24.30.0", "react-hook-form": "^7.58.1", "react-i18next": "^15.5.1", "react-router-dom": "^6.26.0", diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts index a60de91f..f0cd37d3 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts @@ -21,7 +21,7 @@ export class IssuedInvoiceVerifactuFullPresenter extends Presenter { }), () => ({ id: "", - status: "", + status: "Pendiente", url: "", qr_code: "", }) diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts index ab5c29a7..44c76eca 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts @@ -10,6 +10,7 @@ type ReportIssuedInvoiceUseCaseInput = { companyId: UniqueID; companySlug: string; invoice_id: string; + format: "pdf" | "html"; }; export class ReportIssuedInvoiceUseCase { @@ -20,7 +21,7 @@ export class ReportIssuedInvoiceUseCase { ) {} public async execute(params: ReportIssuedInvoiceUseCaseInput) { - const { invoice_id, companyId, companySlug } = params; + const { invoice_id, companyId, companySlug, format } = params; const idOrError = UniqueID.create(invoice_id); @@ -32,7 +33,7 @@ export class ReportIssuedInvoiceUseCase { const pdfPresenter = this.presenterRegistry.getPresenter({ resource: "issued-invoice", projection: "REPORT", - format: "PDF", + format, }) as IssuedInvoiceReportPDFPresenter; return this.transactionManager.complete(async (transaction) => { @@ -48,10 +49,18 @@ export class ReportIssuedInvoiceUseCase { } const invoice = invoiceOrError.data; - const pdfData = await pdfPresenter.toOutput(invoice, { companySlug }); + const reportData = await pdfPresenter.toOutput(invoice, { companySlug }); + + if (format === "html") { + return Result.ok({ + data: String(reportData), + filename: undefined, + }); + } + return Result.ok({ - data: pdfData, - filename: `invoice-${invoice.invoiceNumber}.pdf`, + data: reportData as Buffer, + filename: `proforma-${invoice.invoiceNumber}.pdf`, }); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts index 7b965275..770f96d5 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts @@ -1,12 +1,12 @@ import { Presenter } from "@erp/core/api"; import puppeteer from "puppeteer"; -import report from "puppeteer-report"; import type { CustomerInvoice } from "../../../../../domain"; import type { IssuedInvoiceReportHTMLPresenter } from "./issued-invoice.report.html"; // https://plnkr.co/edit/lWk6Yd?preview +// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports export class IssuedInvoiceReportPDFPresenter extends Presenter< CustomerInvoice, @@ -27,35 +27,35 @@ export class IssuedInvoiceReportPDFPresenter extends Presenter< // Generar el PDF con Puppeteer const browser = await puppeteer.launch({ - headless: "new", + //headless: "new", executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, args: ["--font-render-hinting=medium"], }); const page = await browser.newPage(); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(60000); + //page.setDefaultNavigationTimeout(60000); + //page.setDefaultTimeout(60000); await page.setContent(htmlData, { - waitUntil: "networkidle0", + waitUntil: "networkidle2", }); // Espera extra opcional si hay imágenes base64 muy grandes await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); - const reportPDF = await report.pdfPage(page, { + const reportPDF = await page.pdf({ format: "A4", margin: { - bottom: "10mm", - left: "10mm", - right: "10mm", - top: "10mm", + top: 0, + left: 0, + right: 0, + bottom: 0, }, landscape: false, preferCSSPageSize: true, omitBackground: false, printBackground: true, - displayHeaderFooter: false, + displayHeaderFooter: true, headerTemplate: "
", footerTemplate: '
Página de
', diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts index 33393f84..c1b948ba 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts @@ -6,7 +6,6 @@ import { IssueCustomerInvoiceDomainService, ProformaCustomerInvoiceDomainService, } from "../../../domain"; -import type { ProformaFullPresenter } from "../../presenters"; import type { CustomerInvoiceApplicationService } from "../../services"; type IssueProformaUseCaseInput = { @@ -42,10 +41,6 @@ export class IssueProformaUseCase { if (idOrError.isFailure) return Result.fail(idOrError.error); const proformaId = idOrError.data; - const presenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "FULL", - }) as ProformaFullPresenter; return this.transactionManager.complete(async (transaction) => { try { @@ -97,7 +92,12 @@ export class IssueProformaUseCase { transaction ); - const dto = presenter.toOutput(saveInvoiceResult.data); + const invoice = saveInvoiceResult.data; + const dto = { + proforma_id: proforma.id.toString(), + invoice_id: invoice.id.toString(), + customer_id: invoice.customerId.toString(), + }; return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts index a8b6efad..32efd399 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts @@ -4,12 +4,11 @@ import { Result } from "@repo/rdx-utils"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; -import type { ProformaReportPDFPresenter } from "./reporter"; - type ReportProformaUseCaseInput = { companyId: UniqueID; companySlug: string; proforma_id: string; + format: "pdf" | "html"; }; export class ReportProformaUseCase { @@ -20,7 +19,7 @@ export class ReportProformaUseCase { ) {} public async execute(params: ReportProformaUseCaseInput) { - const { proforma_id, companySlug, companyId } = params; + const { proforma_id, companySlug, companyId, format } = params; const idOrError = UniqueID.create(proforma_id); @@ -29,11 +28,11 @@ export class ReportProformaUseCase { } const proformaId = idOrError.data; - const pdfPresenter = this.presenterRegistry.getPresenter({ + const reportPresenter = this.presenterRegistry.getPresenter({ resource: "proforma", projection: "REPORT", - format: "PDF", - }) as ProformaReportPDFPresenter; + format, + }); return this.transactionManager.complete(async (transaction) => { try { @@ -47,9 +46,17 @@ export class ReportProformaUseCase { } const proforma = proformaOrError.data; - const pdfData = await pdfPresenter.toOutput(proforma, { companySlug }); + const reportData = await reportPresenter.toOutput(proforma, { companySlug }); + + if (format === "html") { + return Result.ok({ + data: String(reportData), + filename: undefined, + }); + } + return Result.ok({ - data: pdfData, + data: reportData as Buffer, filename: `proforma-${proforma.invoiceNumber}.pdf`, }); } catch (error: unknown) { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts index 739ba731..885057a1 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts @@ -1,12 +1,12 @@ import { Presenter } from "@erp/core/api"; import puppeteer from "puppeteer"; -import report from "puppeteer-report"; import type { CustomerInvoice } from "../../../../../domain"; import type { ProformaReportHTMLPresenter } from "./proforma.report.html"; // https://plnkr.co/edit/lWk6Yd?preview +// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports export class ProformaReportPDFPresenter extends Presenter< CustomerInvoice, @@ -25,38 +25,37 @@ export class ProformaReportPDFPresenter extends Presenter< const htmlData = htmlPresenter.toOutput(proforma, params); - // Generar el PDF con Puppeteer // Generar el PDF con Puppeteer const browser = await puppeteer.launch({ - headless: "new", + //headless: "new", executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, args: ["--font-render-hinting=medium"], }); const page = await browser.newPage(); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(60000); + //page.setDefaultNavigationTimeout(60000); + //page.setDefaultTimeout(60000); await page.setContent(htmlData, { - waitUntil: "networkidle0", + waitUntil: "networkidle2", }); // Espera extra opcional si hay imágenes base64 muy grandes await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); - const reportPDF = await report.pdfPage(page, { + const reportPDF = await page.pdf({ format: "A4", margin: { - bottom: "10mm", - left: "10mm", - right: "10mm", - top: "10mm", + bottom: 0, + left: 0, + right: 0, + top: 0, }, landscape: false, preferCSSPageSize: true, omitBackground: false, printBackground: true, - displayHeaderFooter: false, + displayHeaderFooter: true, headerTemplate: "
", footerTemplate: '
Página de
', diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts index b45bed29..19972d66 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts @@ -20,11 +20,13 @@ export class ReportIssuedInvoiceController extends ExpressController { const { companySlug } = this.getUser(); const { invoice_id } = this.req.params; + const { format } = this.req.query as { format: "pdf" | "html" }; - const result = await this.useCase.execute({ invoice_id, companyId, companySlug }); + const result = await this.useCase.execute({ invoice_id, companyId, companySlug, format }); return result.match( - ({ data, filename }) => this.downloadPDF(data, filename), + ({ data, filename }) => + filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string), (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts index d4b89453..822f09c2 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts @@ -20,11 +20,13 @@ 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 result = await this.useCase.execute({ proforma_id, companyId, companySlug }); + const result = await this.useCase.execute({ proforma_id, companyId, companySlug, format }); return result.match( - ({ data, filename }) => this.downloadPDF(data, filename), + ({ data, filename }) => + filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string), (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts index a1140437..9884a32c 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts @@ -7,7 +7,8 @@ import type { Sequelize } from "sequelize"; import { GetIssueInvoiceByIdRequestSchema, ListIssuedInvoicesRequestSchema, - ReportIssueInvoiceByIdRequestSchema, + ReportIssueInvoiceByIdParamsRequestSchema, + ReportIssueInvoiceByIdQueryRequestSchema, } from "../../../common/dto"; import { buildIssuedInvoicesDependencies } from "../issued-invoices-dependencies"; @@ -71,7 +72,8 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { router.get( "/:invoice_id/report", //checkTabContext, - validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"), + validateRequest(ReportIssueInvoiceByIdParamsRequestSchema, "params"), + validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.useCases.report_issued_invoice(); const controller = new ReportIssuedInvoiceController(useCase); diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts index 40fe13b5..7ba284bc 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts @@ -12,7 +12,8 @@ import { GetProformaByIdRequestSchema, IssueProformaByIdParamsRequestSchema, ListProformasRequestSchema, - ReportProformaByIdRequestSchema, + ReportProformaByIdParamsRequestSchema, + ReportProformaByIdQueryRequestSchema, UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, } from "../../../common"; @@ -121,7 +122,8 @@ export const proformasRouter = (params: ModuleParams) => { router.get( "/:proforma_id/report", //checkTabContext, - validateRequest(ReportProformaByIdRequestSchema, "params"), + validateRequest(ReportProformaByIdParamsRequestSchema, "params"), + validateRequest(ReportProformaByIdQueryRequestSchema, "query"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.useCases.report_proforma(); const controller = new ReportProformaController(useCase); diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index 4db47200..fe2413fa 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -78,8 +78,6 @@ export class CustomerInvoiceRepository }); const dtoResult = mapper.mapToPersistence(invoice); - console.log("DTO to persist:", dtoResult); - if (dtoResult.isFailure) { return Result.fail(dtoResult.error); } @@ -358,6 +356,12 @@ export class CustomerInvoiceRepository ], include: [ ...normalizedInclude, + { + model: VerifactuRecordModel, + as: "verifactu", + required: false, + attributes: ["id", "estado", "url", "uuid"], + }, { model: CustomerModel, as: "current_customer", diff --git a/modules/customer-invoices/src/common/dto/request/issued-invoices/report-issued-invoice-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/issued-invoices/report-issued-invoice-by-id.request.dto.ts index 34cfea7f..3ace8985 100644 --- a/modules/customer-invoices/src/common/dto/request/issued-invoices/report-issued-invoice-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/issued-invoices/report-issued-invoice-by-id.request.dto.ts @@ -1,7 +1,17 @@ import { z } from "zod/v4"; -export const ReportIssueInvoiceByIdRequestSchema = z.object({ +export const ReportIssueInvoiceByIdParamsRequestSchema = z.object({ invoice_id: z.string(), }); -export type ReportIssueInvoiceByIdRequestDTO = z.infer; +export type ReportIssueInvoiceByIdParamsRequestDTO = z.infer< + typeof ReportIssueInvoiceByIdParamsRequestSchema +>; + +export const ReportIssueInvoiceByIdQueryRequestSchema = z.object({ + format: z.enum(["pdf", "html"]).default("pdf"), +}); + +export type ReportIssueInvoiceByIdQueryRequestDTO = z.infer< + typeof ReportIssueInvoiceByIdQueryRequestSchema +>; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts index f2ff55a8..9ea45e5a 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/report-proforma-by-id.request.dto.ts @@ -1,7 +1,16 @@ import { z } from "zod/v4"; -export const ReportProformaByIdRequestSchema = z.object({ +export const ReportProformaByIdParamsRequestSchema = z.object({ proforma_id: z.string(), }); -export type ReportProformaByIdRequestDTO = z.infer; +export type ReportProformaByIdParamsRequestDTO = z.infer< + typeof ReportProformaByIdParamsRequestSchema +>; +export const ReportProformaByIdQueryRequestSchema = z.object({ + format: z.enum(["pdf", "html"]).default("pdf"), +}); + +export type ReportProformaByIdQueryRequestDTO = z.infer< + typeof ReportProformaByIdQueryRequestSchema +>; diff --git a/modules/customer-invoices/templates/acana/issued-invoice.hbs b/modules/customer-invoices/templates/acana/issued-invoice.hbs index c0014c82..b84980f7 100644 --- a/modules/customer-invoices/templates/acana/issued-invoice.hbs +++ b/modules/customer-invoices/templates/acana/issued-invoice.hbs @@ -3,13 +3,7 @@ - + Factura + Factura proforma -