diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a8582dec..d93500f9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,12 @@ { "recommendations": [ + "mfeckies.handlebars-formatter", "bradlc.vscode-tailwindcss", "biomejs.biome", "cweijan.vscode-mysql-client2", "ms-vscode.vscode-json", "formulahendry.auto-rename-tag", - "cweijan.dbclient-jdbc" + "cweijan.dbclient-jdbc", + "nabous.handlebars-preview-plus" ] } diff --git a/apps/server/.env.development b/apps/server/.env.development index eb2cb0e9..d00c7f3f 100644 --- a/apps/server/.env.development +++ b/apps/server/.env.development @@ -23,4 +23,10 @@ JWT_REFRESH_EXPIRATION=7d PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome -TEMPLATES_PATH=/home/rodax/Documentos/uecko-erp/modules \ No newline at end of file +TEMPLATES_PATH=/home/rodax/Documentos/uecko-erp/modules +DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/documents + +SIGN_DOCUMENTS=false +SIGNED_DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/signed-documents + +FASTREPORT_BIN=/home/rodax/Documentos/uecko-erp/tools/fastreport-cli/publish/linux/FastReportCliGenerator \ No newline at end of file diff --git a/apps/server/.env.rodax b/apps/server/.env.rodax index 239f987a..4d9e2920 100644 --- a/apps/server/.env.rodax +++ b/apps/server/.env.rodax @@ -32,6 +32,14 @@ API_IMAGE=factuges-server:rodax-latest # Plantillas TEMPLATES_PATH=/repo/apps/server/templates +# Documentos generados +DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/documents + +# Firma de documentos +SIGN_DOCUMENTS=false +SIGNED_DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/signed-documents + + # Chrome executable path (Puppeteer) PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome diff --git a/apps/server/archive/contexts/auth/infraestructure/express/types.ts b/apps/server/archive/contexts/auth/infraestructure/express/types.ts index eba23dea..1b7a6ff6 100644 --- a/apps/server/archive/contexts/auth/infraestructure/express/types.ts +++ b/apps/server/archive/contexts/auth/infraestructure/express/types.ts @@ -1,5 +1,6 @@ -import { Request } from "express"; -import { AuthenticatedUser, TabContext } from "../../domain"; +import type { Request } from "express"; + +import type { AuthenticatedUser, TabContext } from "../../domain"; export interface TabContextRequest extends Request { tabContext?: TabContext; diff --git a/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts b/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts index d1425975..1342c9e8 100644 --- a/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts +++ b/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts @@ -1,17 +1,19 @@ +//import { authProvider } from "@/contexts/auth/infraestructure"; +import type { NextFunction, Response } from "express"; + import { UniqueID } from "@/core/common/domain"; import { ApiError, ExpressController } from "@/core/common/presentation"; -//import { authProvider } from "@/contexts/auth/infraestructure"; -import { NextFunction, Response } from "express"; + import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport"; -import { AuthenticatedUser } from "../../domain"; -import { AuthenticatedRequest } from "../express/types"; +import type { AuthenticatedUser } from "../../domain"; +import type { AuthenticatedRequest } from "../express/types"; // Comprueba el rol del usuario const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const user = req.user as AuthenticatedUser; - if (!user || !condition(user)) { + if (!(user && condition(user))) { return ExpressController.errorResponse( new ApiError({ status: 401, diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts index 9339160b..08484875 100644 --- a/apps/server/src/config/index.ts +++ b/apps/server/src/config/index.ts @@ -39,9 +39,16 @@ const DB_SYNC_MODE = // Opcional: timezone para Sequelize (según necesidades) const APP_TIMEZONE = process.env.APP_TIMEZONE ?? "Europe/Madrid"; +// FASTREPORT_BIN: ruta al ejecutable de FastReport CLI +// Si no se define, se buscará en rutas estándar según SO. +const FASTREPORT_BIN = process.env.FASTREPORT_BIN; + // Ruta raíz para plantillas (templates) const TEMPLATES_PATH = process.env.TEMPLATES_PATH ?? "./templates"; +// Documentos +const DOCUMENTS_PATH = process.env.DOCUMENTS_PATH ?? "./documents"; + // Proxy (no usáis ahora, pero dejamos la variable por si se activa en el futuro) const TRUST_PROXY = asNumber(process.env.TRUST_PROXY, 0); @@ -61,6 +68,8 @@ export const ENV = { APP_TIMEZONE, TRUST_PROXY, TEMPLATES_PATH, + DOCUMENTS_PATH, + FASTREPORT_BIN, } as const; export const Flags = { diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index ff88fb9b..9ca7572d 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -222,6 +222,7 @@ process.on("uncaughtException", async (error: Error) => { logger.info(`DB_LOGGING: ${ENV.DB_LOGGING}`); logger.info(`DB_SYNC_MODE: ${ENV.DB_SYNC_MODE}`); + logger.info(`FASTREPORT_BIN: ${ENV.FASTREPORT_BIN}`); logger.info(`TEMPLATES_PATH: ${ENV.TEMPLATES_PATH}`); const database = await tryConnectToDatabase(); @@ -234,11 +235,11 @@ process.on("uncaughtException", async (error: Error) => { registerHealthRoutes(app, API_BASE_PATH, () => ({ ready: isReady })); await initModules({ + env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger, - templateRootPath: ENV.TEMPLATES_PATH, }); // El servidor ya está listo para recibir tráfico diff --git a/apps/server/src/lib/modules/model-loader.ts b/apps/server/src/lib/modules/model-loader.ts index 230f9e81..a7c8abf4 100644 --- a/apps/server/src/lib/modules/model-loader.ts +++ b/apps/server/src/lib/modules/model-loader.ts @@ -1,4 +1,5 @@ -import { ModuleParams } from "@erp/core/api"; +import type { ModuleParams } from "@erp/core/api"; + import { ENV } from "../../config"; import { logger } from "../logger"; diff --git a/modules/auth/src/api/lib/express/index.ts b/modules/auth/src/api/lib/express/index.ts index c4ccac27..c39f7e57 100644 --- a/modules/auth/src/api/lib/express/index.ts +++ b/modules/auth/src/api/lib/express/index.ts @@ -1,4 +1,3 @@ -export * from "./auth-types"; export * from "./mock-user.middleware"; -export * from "./tenancy.middleware"; -export * from "./user.middleware"; +export * from "./require-authenticated.middleware"; +export * from "./require-company-context.middleware"; diff --git a/modules/auth/src/api/lib/express/user.middleware.ts b/modules/auth/src/api/lib/express/require-authenticated.middleware.ts similarity index 92% rename from modules/auth/src/api/lib/express/user.middleware.ts rename to modules/auth/src/api/lib/express/require-authenticated.middleware.ts index 6937550f..8b2ec659 100644 --- a/modules/auth/src/api/lib/express/user.middleware.ts +++ b/modules/auth/src/api/lib/express/require-authenticated.middleware.ts @@ -7,7 +7,7 @@ import type { RequestWithAuth } from "./auth-types"; * Middleware que exige presencia de usuario (sin validar companyId). * Debe ir DESPUÉS del middleware de autenticación. */ -export function enforceUser() { +export function requireAuthenticated() { return (req: RequestWithAuth, res: Response, next: NextFunction) => { if (!req.user) { return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); diff --git a/modules/auth/src/api/lib/express/tenancy.middleware.ts b/modules/auth/src/api/lib/express/require-company-context.middleware.ts similarity index 61% rename from modules/auth/src/api/lib/express/tenancy.middleware.ts rename to modules/auth/src/api/lib/express/require-company-context.middleware.ts index ea943771..6297c714 100644 --- a/modules/auth/src/api/lib/express/tenancy.middleware.ts +++ b/modules/auth/src/api/lib/express/require-company-context.middleware.ts @@ -1,4 +1,4 @@ -import { ExpressController, UnauthorizedApiError } from "@erp/core/api"; +import { ExpressController, ForbiddenApiError } from "@erp/core/api"; import type { NextFunction, Response } from "express"; import type { RequestWithAuth } from "./auth-types"; @@ -7,11 +7,15 @@ import type { RequestWithAuth } from "./auth-types"; * Middleware que exige presencia de usuario y companyId. * Debe ir DESPUÉS del middleware de autenticación. */ -export function enforceTenant() { +export function requireCompanyContext() { return (req: RequestWithAuth, res: Response, next: NextFunction) => { // Validación básica del tenant if (!req.user?.companyId) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + return ExpressController.errorResponse( + new ForbiddenApiError("Company context required"), + req, + res + ); } next(); }; diff --git a/modules/core/src/api/application/index.ts b/modules/core/src/api/application/index.ts index aee6f87b..c7df6428 100644 --- a/modules/core/src/api/application/index.ts +++ b/modules/core/src/api/application/index.ts @@ -1,2 +1,3 @@ export * from "./errors"; export * from "./presenters"; +export * from "./renderers"; diff --git a/modules/core/src/api/application/presenters/index.ts b/modules/core/src/api/application/presenters/index.ts index aa2c6419..91069846 100644 --- a/modules/core/src/api/application/presenters/index.ts +++ b/modules/core/src/api/application/presenters/index.ts @@ -2,4 +2,3 @@ export * from "./presenter"; export * from "./presenter.interface"; export * from "./presenter-registry"; export * from "./presenter-registry.interface"; -export * from "./template-presenter"; diff --git a/modules/core/src/api/application/presenters/partials/index.ts b/modules/core/src/api/application/presenters/partials/index.ts deleted file mode 100644 index e69de29b..00000000 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 2a1ec137..1530f8b1 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.interface.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.interface.ts @@ -1,3 +1,5 @@ +import type { DTO } from "@erp/core/common"; + import type { IPresenter } from "./presenter.interface"; /** @@ -6,53 +8,29 @@ import type { IPresenter } from "./presenter.interface"; export type PresenterKey = { resource: string; projection: "FULL" | "LIST" | "REPORT" | (string & {}); - format?: "JSON" | "HTML" | "PDF" | "CSV" | (string & {}); + format?: "DTO"; version?: number; // 1 | 2 locale?: string; // es | en | fr }; -/** - * Ejemplo de uso: - * - * const registry = new InMemoryPresenterRegistry(); - * - * // Registro - * registry.register( - * { resource: "customer-invoice", projection: "detail", format: "json", version: 1 }, - * new CustomerInvoiceDetailPresenter() - * ); - * - * registry.register( - * { resource: "customer-invoice", projection: "detail", format: "pdf", version: 1 }, - * new CustomerInvoicePdfPresenter() - * ); - * - * // Resolución - * const presenterOrNone = registry.resolve({ - * resource: "customer-invoice", - * projection: "detail", - * format: "pdf", - * }); - * - * presenterOrNone.map(async (presenter) => { - * const output = await (presenter as IAsyncPresenter).toOutput(invoice); - * console.log("PDF generado:", output); - * }); - * - **/ +export type PresenterFormat = NonNullable; + +export type PresenterFormatOutputMap = { + DTO: DTO; +}; export interface IPresenterRegistry { /** * Obtiene un mapper de dominio por clave de proyección. */ - getPresenter( - key: PresenterKey - ): IPresenter; + getPresenter( + key: Omit & { format?: F } + ): IPresenter; /** * Registra un mapper de dominio bajo una clave de proyección. */ - registerPresenter( + registerPresenter( key: PresenterKey, presenter: IPresenter ): this; diff --git a/modules/core/src/api/application/presenters/presenter-registry.ts b/modules/core/src/api/application/presenters/presenter-registry.ts index 183dfd7c..1d80c9ce 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.ts @@ -9,7 +9,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { private _normalizeKey(key: PresenterKey): PresenterKey { return { ...key, - format: key.format ?? "JSON", // 👈 valor por defecto + format: key.format ?? "DTO", // 👈 valor por defecto }; } diff --git a/modules/core/src/api/application/presenters/presenter.interface.ts b/modules/core/src/api/application/presenters/presenter.interface.ts index 25029d76..ca0605bb 100644 --- a/modules/core/src/api/application/presenters/presenter.interface.ts +++ b/modules/core/src/api/application/presenters/presenter.interface.ts @@ -1,8 +1,7 @@ -export type DTO = T; -export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams +import type { DTO } from "../../../common/types"; export type IPresenterOutputParams = Record; -export interface IPresenter { +export interface IPresenter { toOutput(source: TSource, params?: IPresenterOutputParams): TOutput | Promise; } diff --git a/modules/core/src/api/application/presenters/template-presenter.ts b/modules/core/src/api/application/presenters/template-presenter.ts deleted file mode 100644 index 767d7ddd..00000000 --- a/modules/core/src/api/application/presenters/template-presenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HandlebarsTemplateResolver } from "../../infrastructure"; - -import { Presenter } from "./presenter"; -import type { IPresenter } from "./presenter.interface"; -import type { IPresenterRegistry } from "./presenter-registry.interface"; - -export abstract class TemplatePresenter - extends Presenter - implements IPresenter -{ - constructor( - protected presenterRegistry: IPresenterRegistry, - protected templateResolver: HandlebarsTemplateResolver - ) { - super(presenterRegistry); - } -} diff --git a/modules/core/src/api/application/renderers/index.ts b/modules/core/src/api/application/renderers/index.ts new file mode 100644 index 00000000..13911d6d --- /dev/null +++ b/modules/core/src/api/application/renderers/index.ts @@ -0,0 +1,5 @@ +export * from "./renderer"; +export * from "./renderer.interface"; +export * from "./renderer-registry"; +export * from "./renderer-registry.interface"; +export * from "./renderer-template-resolver.interface"; diff --git a/modules/core/src/api/application/renderers/renderer-registry.interface.ts b/modules/core/src/api/application/renderers/renderer-registry.interface.ts new file mode 100644 index 00000000..61c64a08 --- /dev/null +++ b/modules/core/src/api/application/renderers/renderer-registry.interface.ts @@ -0,0 +1,36 @@ +import type { IRenderer } from "./renderer.interface"; + +export type RendererFormat = "HTML" | "PDF"; + +/** + * 🔑 Claves de proyección comunes para seleccionar Renderers + */ +export type RendererKey = { + resource: string; + format?: RendererFormat; + version?: number; // 1 | 2 + locale?: string; // es | en | fr +}; + +export type RendererFormatOutputMap = { + HTML: string; + PDF: Buffer; +}; + +export interface IRendererRegistry { + /** + * Obtiene un mapper de dominio por clave de proyección. + */ + getRenderer( + key: RendererKey + ): IRenderer; + + /** + * Registra un mapper de dominio bajo una clave de proyección. + */ + registerRenderer(key: RendererKey, renderer: IRenderer): this; + + registerRenderers( + renderers: Array<{ key: RendererKey; renderer: IRenderer }> + ): this; +} diff --git a/modules/core/src/api/application/renderers/renderer-registry.ts b/modules/core/src/api/application/renderers/renderer-registry.ts new file mode 100644 index 00000000..7bb21c78 --- /dev/null +++ b/modules/core/src/api/application/renderers/renderer-registry.ts @@ -0,0 +1,92 @@ +import { ApplicationError } from "../errors"; + +import type { IRenderer } from "./renderer.interface"; +import type { IRendererRegistry, RendererKey } from "./renderer-registry.interface"; + +export class InMemoryRendererRegistry implements IRendererRegistry { + private registry: Map> = new Map(); + + private _normalizeKey(key: RendererKey): RendererKey { + return { + ...key, + format: key.format ?? "PDF", // 👈 valor por defecto + }; + } + + /** + * 🔹 Construye la clave única para el registro. + */ + private _buildKey(key: RendererKey): string { + const { resource, format, version, locale } = this._normalizeKey(key); + return [ + resource.toLowerCase(), + format!.toLowerCase(), + version ?? "latest", + locale ?? "default", + ].join("::"); + } + + private _registerRenderer( + key: RendererKey, + renderer: IRenderer + ): void { + const exactKey = this._buildKey(key); + this.registry.set(exactKey, renderer); + } + + getRenderer(key: RendererKey): IRenderer { + const exactKey = this._buildKey(key); + + // 1) Intentar clave exacta + if (this.registry.has(exactKey)) { + return this.registry.get(exactKey)!; + } + + // 2) Fallback por versión: si no se indicó, buscar la última registrada + if (key.version === undefined) { + const candidates = [...this.registry.keys()].filter((k) => + k.startsWith(this._buildKey({ ...key, version: undefined, locale: undefined })) + ); + + if (candidates.length > 0) { + const latest = candidates.sort().pop()!; // simplificación: versión más alta lexicográficamente + return this.registry.get(latest)!; + } + } + + // 3) Fallback por locale: intentar sin locale si no se encuentra exacto + if (key.locale) { + const withoutLocale = this._buildKey({ ...key, locale: undefined }); + if (this.registry.has(withoutLocale)) { + return this.registry.get(withoutLocale)!; + } + } + + if (!this.registry.has(exactKey)) { + throw new ApplicationError( + `[InMemoryRendererRegistry] Renderer not registered: ${key.resource}::${key.format}` + ); + } + + throw new ApplicationError( + `[InMemoryRendererRegistry] Renderer not registered: ${key.resource}::${key.format}` + ); + } + + registerRenderer( + key: RendererKey, + renderer: IRenderer + ): this { + this._registerRenderer(key, renderer); + return this; + } + /** + * ✅ Registro en lote de renderers. + */ + registerRenderers(renderers: Array<{ key: RendererKey; renderer: IRenderer }>): this { + for (const { key, renderer } of renderers) { + this._registerRenderer(key, renderer); + } + return this; + } +} diff --git a/modules/core/src/api/application/renderers/renderer-template-resolver.interface.ts b/modules/core/src/api/application/renderers/renderer-template-resolver.interface.ts new file mode 100644 index 00000000..3878b0d5 --- /dev/null +++ b/modules/core/src/api/application/renderers/renderer-template-resolver.interface.ts @@ -0,0 +1,4 @@ +export interface IRendererTemplateResolver { + /** Devuelve la ruta absoluta del fichero de plantilla */ + resolveTemplatePath(module: string, companySlug: string, templateName: string): string; +} diff --git a/modules/core/src/api/application/renderers/renderer.interface.ts b/modules/core/src/api/application/renderers/renderer.interface.ts new file mode 100644 index 00000000..d8461465 --- /dev/null +++ b/modules/core/src/api/application/renderers/renderer.interface.ts @@ -0,0 +1,5 @@ +export type IRendererParams = Record; + +export interface IRenderer { + render(source: TSource, params?: IRendererParams): TOutput | Promise; +} diff --git a/modules/core/src/api/application/renderers/renderer.ts b/modules/core/src/api/application/renderers/renderer.ts new file mode 100644 index 00000000..701247c3 --- /dev/null +++ b/modules/core/src/api/application/renderers/renderer.ts @@ -0,0 +1,7 @@ +import type { IRenderer, IRendererParams } from "./renderer.interface"; + +export abstract class Renderer + implements IRenderer +{ + abstract render(source: TSource, params?: IRendererParams): TOutput | Promise; +} diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts index 37f0b870..c2a7f2f4 100644 --- a/modules/core/src/api/infrastructure/express/api-error-mapper.ts +++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts @@ -30,6 +30,10 @@ import { isInfrastructureRepositoryError, isInfrastructureUnavailableError, } from "../errors"; +import { + type FastReportError, + isFastReportError, +} from "../renderers/fast-report/fastreport-errors"; import { ApiError, @@ -159,6 +163,14 @@ const defaultRules: ReadonlyArray = [ ), }, + // 5.5) Errores de FastReport inesperados + { + priority: 55, + matches: (e) => isFastReportError(e), + build: (e) => + new InternalApiError((e as FastReportError).message, "Unexpected FastReport error"), + }, + // 6) Infra no transitoria (errores de repositorio inesperados) { priority: 50, diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index ea5463f8..16cc1333 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -135,6 +135,14 @@ export abstract class ExpressController { return this.res.send(htmlString); } + public json(data: any) { + this.res.set({ + "Content-Type": "application/json; charset=utf-8", + }); + + return this.res.json(data); + } + 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/express/express-guards.ts b/modules/core/src/api/infrastructure/express/express-guards.ts index e3e483d1..64a9ac62 100644 --- a/modules/core/src/api/infrastructure/express/express-guards.ts +++ b/modules/core/src/api/infrastructure/express/express-guards.ts @@ -31,18 +31,20 @@ export function guardFail(error: ApiError): GuardResultLike { // ─────────────────────────────────────────────────────────────────────────── // Guards reutilizables (auth/tenancy). Si prefieres, muévelos a src/lib/http/express-guards.ts -export function authGuard(): GuardFn { - return ({ controller }: GuardContext) => { - const user = controller.getUser(); - if (!user) return guardFail(new UnauthorizedApiError("Unauthorized")); +export function requireAuthenticatedGuard(): GuardFn { + return ({ controller }) => { + if (!controller.getUser()) { + return guardFail(new UnauthorizedApiError("Unauthorized")); + } return guardOk(); }; } -export function tenantGuard(): GuardFn { - return ({ controller }: GuardContext) => { - const tenantId = controller.getTenantId(); - if (!tenantId) return guardFail(new ForbiddenApiError("Tenant not found for user")); +export function requireCompanyContextGuard(): GuardFn { + return ({ controller }) => { + if (!controller.getTenantId()) { + return guardFail(new ForbiddenApiError("Company context required")); + } return guardOk(); }; } diff --git a/modules/core/src/api/infrastructure/express/index.ts b/modules/core/src/api/infrastructure/express/index.ts index a3c86a72..aa2204a9 100644 --- a/modules/core/src/api/infrastructure/express/index.ts +++ b/modules/core/src/api/infrastructure/express/index.ts @@ -3,3 +3,4 @@ export * from "./errors"; export * from "./express-controller"; export * from "./express-guards"; export * from "./middlewares"; +export * from "./request-with-auth"; diff --git a/modules/core/src/api/infrastructure/express/middlewares/authenticate-request.ts b/modules/core/src/api/infrastructure/express/middlewares/authenticate-request.ts new file mode 100644 index 00000000..841c3b6a --- /dev/null +++ b/modules/core/src/api/infrastructure/express/middlewares/authenticate-request.ts @@ -0,0 +1,122 @@ +import type { EmailAddress, UniqueID } from "@repo/rdx-ddd"; +import type { NextFunction, Response } from "express"; + +import { ForbiddenApiError, UnauthorizedApiError } from "../errors"; +import { ExpressController } from "../express-controller"; +import type { RequestUser, RequestWithAuth } from "../request-with-auth"; + +export type AccessTokenPayload = { + sub: string; + email?: string; + roles?: string[]; + sid?: string; +}; + +export interface AccessTokenVerifier { + verifyAccessToken(token: string): Promise; +} + +export type AuthenticateRequestOptions = { + tokenVerifier: AccessTokenVerifier; + envCompanyId: string; + + companyHeaderName?: string; + authorizationHeaderName?: string; + + defaultRoles?: string[]; + + /** + * Entorno actual. Si es "production", el cliente DEBE enviar X-Company-Id. + * En otros entornos, se inyecta automáticamente si falta. + */ + environment?: "development" | "test" | "production"; +}; + +function getHeader(req: any, name: string): string | undefined { + const value = typeof req.get === "function" ? req.get(name) : undefined; + if (typeof value === "string" && value.trim()) return value.trim(); + + const raw = req.headers?.[name.toLowerCase()]; + if (typeof raw === "string" && raw.trim()) return raw.trim(); + + return undefined; +} + +function parseBearerToken(authorization?: string): string | undefined { + if (!authorization) return undefined; + const [scheme, token] = authorization.split(" "); + if (scheme?.toLowerCase() !== "bearer") return undefined; + return token?.trim() || undefined; +} + +export function authenticateRequest(options: AuthenticateRequestOptions) { + const companyHeaderName = options.companyHeaderName ?? "X-Company-Id"; + const authorizationHeaderName = options.authorizationHeaderName ?? "authorization"; + const defaultRoles = options.defaultRoles ?? ["ADMIN"]; + const environment = options.environment ?? (process.env.NODE_ENV as any) ?? "development"; + + if (!options.envCompanyId?.trim()) { + throw new Error("ENV company id is missing (envCompanyId)"); + } + + return async (req: RequestWithAuth, res: Response, next: NextFunction) => { + try { + // ───────────────────────────────────────── + // 1) Autenticación (JWT) + const authorization = getHeader(req, authorizationHeaderName); + const token = parseBearerToken(authorization); + + if (!token) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const payload = await options.tokenVerifier.verifyAccessToken(token); + if (!payload?.sub) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + // ───────────────────────────────────────── + // 2) Company context + const requestedCompanyId = getHeader(req, companyHeaderName); + let resolvedCompanyId: string | undefined; + + if (requestedCompanyId) { + if (requestedCompanyId !== options.envCompanyId) { + return ExpressController.errorResponse( + new ForbiddenApiError("Invalid company context"), + req, + res + ); + } + resolvedCompanyId = requestedCompanyId; + } else { + // No viene en header + if (environment === "production") { + return ExpressController.errorResponse( + new ForbiddenApiError("Company context required"), + req, + res + ); + } + // Dev/Test → inyección automática + resolvedCompanyId = options.envCompanyId; + } + + // ───────────────────────────────────────── + // 3) Construir contexto de usuario (MVP) + const user: RequestUser = { + userId: payload.sub as unknown as UniqueID, + companyId: resolvedCompanyId as unknown as UniqueID, + companySlug: "default", + roles: payload.roles?.length ? payload.roles : defaultRoles, + email: payload.email as unknown as EmailAddress, + }; + + (req as any).user = user; + + next(); + } catch { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + }; +} diff --git a/modules/auth/src/api/lib/express/auth-types.ts b/modules/core/src/api/infrastructure/express/request-with-auth.ts similarity index 85% rename from modules/auth/src/api/lib/express/auth-types.ts rename to modules/core/src/api/infrastructure/express/request-with-auth.ts index d1f854a8..8f7a88bf 100644 --- a/modules/auth/src/api/lib/express/auth-types.ts +++ b/modules/core/src/api/infrastructure/express/request-with-auth.ts @@ -3,8 +3,8 @@ import type { Request } from "express"; export type RequestUser = { userId: UniqueID; - companyId: UniqueID; - companySlug: string; + companyId?: UniqueID; + companySlug?: string; roles?: string[]; email?: EmailAddress; }; diff --git a/modules/core/src/api/infrastructure/index.ts b/modules/core/src/api/infrastructure/index.ts index c2dde563..6ab7bade 100644 --- a/modules/core/src/api/infrastructure/index.ts +++ b/modules/core/src/api/infrastructure/index.ts @@ -3,5 +3,6 @@ export * from "./errors"; export * from "./express"; export * from "./logger"; export * from "./mappers"; +export * from "./renderers"; +export * from "./reporting"; export * from "./sequelize"; -export * from "./templates"; diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts new file mode 100644 index 00000000..7984ea9b --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts @@ -0,0 +1,53 @@ +import { InfrastructureError } from "../../errors"; + +/** + * Error base de FastReport. + * NO debe cruzar el boundary de Infraestructura sin mapearse. + */ +export abstract class FastReportError extends InfrastructureError {} + +export const isFastReportError = (e: unknown): e is FastReportError => e instanceof FastReportError; + +/** + * El ejecutable FastReport no existe o no es accesible. + */ +export class FastReportExecutableNotFoundError extends FastReportError { + public readonly code = "FASTREPORT_EXE_NOT_FOUND_ERROR" as const; + + public constructor(executable: string, searchedPaths?: string[]) { + super( + searchedPaths + ? `FastReport executable "${executable}" not found. Searched in: ${searchedPaths.join( + ", " + )}` + : `FastReport executable "${executable}" not found` + ); + } +} + +/** + * La plantilla .frx no existe. + */ +export class FastReportTemplateNotFoundError extends FastReportError { + public readonly code = "FASTREPORT_TEMPLATE_NOT_FOUND_ERROR" as const; + + public constructor(templatePath: string) { + super(`FastReport template not found at path: ${templatePath}`); + } +} + +/** + * Error al ejecutar el proceso FastReport. + * Incluye stderr o información del exit code. + */ +export class FastReportExecutionError extends FastReportError { + public readonly code = "FASTREPORT_EXECUTION_ERROR" as const; +} + +/** + * Error de IO relacionado con FastReport + * (lectura/escritura de ficheros temporales). + */ +export class FastReportIOError extends FastReportError { + public readonly code = "FASTREPORT_IO_ERROR" as const; +} diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts new file mode 100644 index 00000000..155b2f32 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts @@ -0,0 +1,51 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { buildSafePath } from "@repo/rdx-utils"; + +import { FastReportExecutableNotFoundError } from "./fastreport-errors"; + +/** + * Resuelve la ruta absoluta del ejecutable FastReport. + * + * Reglas: + * 1. ENV FASTREPORT_BIN (si existe) + * 2. Rutas estándar por SO + * 3. Fail-fast si no existe + */ +export class FastReportExecutableResolver { + public resolve(): string { + const fromEnv = process.env.FASTREPORT_BIN; + if (fromEnv) { + this.assertExecutableExists(fromEnv); + return fromEnv; + } + + const executableName = this.resolveExecutableName(); + const candidatePaths = this.getCandidatePaths(); + + for (const basePath of candidatePaths) { + const fullPath = buildSafePath({ basePath, segments: [], filename: executableName }); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + throw new FastReportExecutableNotFoundError(executableName, candidatePaths); + } + + private resolveExecutableName(): string { + return os.platform() === "win32" ? "FastReportCliGenerator.exe" : "FastReportCliGenerator"; + } + + private getCandidatePaths(): string[] { + return ["/usr/local/bin", "/usr/bin", path.resolve(process.cwd(), "bin")]; + } + + private assertExecutableExists(executablePath: string): void { + if (!fs.existsSync(executablePath)) { + throw new FastReportExecutableNotFoundError(executablePath); + } + } +} diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-process-runner.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-process-runner.ts new file mode 100644 index 00000000..e5970147 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-process-runner.ts @@ -0,0 +1,114 @@ +import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; + +import { Result, buildSafePath } from "@repo/rdx-utils"; + +import { FastReportExecutionError } from "./fastreport-errors"; + +/** + * Ejecuta el binario FastReport como proceso externo. + * + * - Escribe JSON de entrada + * - Ejecuta el proceso + * - Lee el output generado + * - Devuelve Result + */ + +export type FastReportProcessRunnerArgs = { + templatePath: string; // Path to FRX template (required) + data: string; // JSON data as string + format: "PDF" | "HTML"; + workdir: string; // Directorio de trabajo temporal +}; + +export class FastReportProcessRunner { + public async run( + executablePath: string, + executableArgs: FastReportProcessRunnerArgs + ): Promise> { + const { templatePath, data, format, workdir } = 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 args = this.buildArgs({ + templatePath, + dataPath, + outputPath, + format, + }); + + return this.executeProcess(executablePath, args, outputPath, executableArgs.format); + } + + private buildArgs(params: { + templatePath: string; + dataPath: string; + outputPath: string; + format: "PDF" | "HTML"; + }): string[] { + return [ + `--template=${params.templatePath}`, + `--data=${params.dataPath}`, + `--output=${params.outputPath}`, + `--format=${params.format}`, + ]; + } + + private executeProcess( + executablePath: string, + args: string[], + outputPath: string, + format: "PDF" | "HTML" + ): Promise> { + return new Promise((resolve) => { + const child = spawn(executablePath, args, { + stdio: ["ignore", "ignore", "pipe"], + }); + + let stderr = ""; + + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + child.on("error", (error) => { + resolve(Result.fail(new FastReportExecutionError(String(error)))); + }); + + child.on("close", async (code) => { + if (code !== 0) { + return resolve( + Result.fail( + new FastReportExecutionError(stderr || `FastReport exited with code ${code}`) + ) + ); + } + + try { + const output = + format === "PDF" + ? await fs.readFile(outputPath) + : await fs.readFile(outputPath, "utf-8"); + + resolve(Result.ok(output)); + } catch (readError) { + resolve( + Result.fail( + new FastReportExecutionError("Failed to read FastReport output", readError as Error) + ) + ); + } + }); + }); + } +} diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts new file mode 100644 index 00000000..f5dde145 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts @@ -0,0 +1,7 @@ +import type { ReportStorageKey } from "../../reporting"; + +export type FastReportRenderOptions = { + inputData: unknown; + format: "PDF" | "HTML"; + storageKey: ReportStorageKey; +}; diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts new file mode 100644 index 00000000..ec5aad39 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts @@ -0,0 +1,140 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { Maybe, Result } from "@repo/rdx-utils"; + +import { Renderer } from "../../../application"; +import type { ReportStorage, ReportStorageKey } from "../../reporting"; + +import { + type FastReportError, + FastReportExecutionError, + FastReportTemplateNotFoundError, +} from "./fastreport-errors"; +import type { FastReportExecutableResolver } from "./fastreport-executable-resolver"; +import type { FastReportProcessRunner } from "./fastreport-process-runner"; +import type { FastReportRenderOptions } from "./fastreport-render-options.type"; +import type { FastReportTemplateResolver } from "./fastreport-template-resolver"; + +export type FastReportRenderOutput = Result< + { payload: Buffer | string; templateChecksum: string }, + FastReportError +>; + +/** + * Clase base para renderers FastReport. + */ +export abstract class FastReportRenderer extends Renderer { + constructor( + protected readonly executableResolver: FastReportExecutableResolver, + protected readonly processRunner: FastReportProcessRunner, + protected readonly templateResolver: FastReportTemplateResolver, + protected readonly reportStorage: ReportStorage + ) { + super(); + } + + /** + * Punto de entrada común para renderizado. + */ + protected async renderInternal( + options: FastReportRenderOptions + ): Promise { + if (process.env.NODE_ENV !== "development") { + // Cache (read-through) + const cached = await this.tryReadFromCache(options.storageKey); + if (cached.isSome()) { + return Result.ok({ + payload: cached.unwrap(), + templateChecksum: "CACHED", + }); + } + } + + try { + // Resolver plantilla + const templatePath = this.resolveTemplatePath(); + console.log("Using FastReport template:", templatePath); + + if (!fs.existsSync(templatePath)) { + return Result.fail(new FastReportTemplateNotFoundError(templatePath)); + } + + const templateChecksum = this.calculateChecksum(templatePath); + + // Llamar a FastReport + const callResult = await this.callFastReportGenerator(options, templatePath); + + if (callResult.isFailure) { + return Result.fail(callResult.error); + } + + // Guardar documento generado (best-effort) + await this.storageReport(options.storageKey, callResult.data); + + return Result.ok({ + payload: callResult.data, + templateChecksum, + }); + } catch (error) { + console.error("FastReport rendering error:", error); + return Result.fail( + new FastReportExecutionError("Unexpected FastReport rendering error", error as Error) + ); + } + } + + 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-")); + } + + /** + * Cada renderer concreto decide + * qué plantilla usar. + */ + 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/renderers/fast-report/fastreport-template-resolver.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-template-resolver.ts new file mode 100644 index 00000000..68a339b3 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-template-resolver.ts @@ -0,0 +1,3 @@ +import { RendererTemplateResolver } from "../renderer-template-resolver"; + +export class FastReportTemplateResolver extends RendererTemplateResolver {} diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/index.ts b/modules/core/src/api/infrastructure/renderers/fast-report/index.ts new file mode 100644 index 00000000..575cf6a2 --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/fast-report/index.ts @@ -0,0 +1,6 @@ +export * from "./fastreport-errors"; +export * from "./fastreport-executable-resolver"; +export * from "./fastreport-process-runner"; +export * from "./fastreport-render-options.type"; +export * from "./fastreport-renderer.base"; +export * from "./fastreport-template-resolver"; diff --git a/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts b/modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts similarity index 95% rename from modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts rename to modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts index a2a52301..54644706 100644 --- a/modules/core/src/api/infrastructure/templates/handlebars-template-resolver.ts +++ b/modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts @@ -3,9 +3,9 @@ import { existsSync, readFileSync } from "node:fs"; import Handlebars from "handlebars"; import { lookup } from "mime-types"; -import { TemplateResolver } from "./template-resolver"; +import { RendererTemplateResolver } from "../renderer-template-resolver"; -export class HandlebarsTemplateResolver extends TemplateResolver { +export class HandlebarsTemplateResolver extends RendererTemplateResolver { protected readonly hbs = Handlebars.create(); protected registered = false; protected readonly assetCache = new Map(); diff --git a/modules/core/src/api/infrastructure/templates/index.ts b/modules/core/src/api/infrastructure/renderers/handlebars/index.ts similarity index 56% rename from modules/core/src/api/infrastructure/templates/index.ts rename to modules/core/src/api/infrastructure/renderers/handlebars/index.ts index 1fd44480..1ae3840e 100644 --- a/modules/core/src/api/infrastructure/templates/index.ts +++ b/modules/core/src/api/infrastructure/renderers/handlebars/index.ts @@ -1,2 +1 @@ export * from "./handlebars-template-resolver"; -export * from "./template-resolver"; diff --git a/modules/core/src/api/infrastructure/renderers/index.ts b/modules/core/src/api/infrastructure/renderers/index.ts new file mode 100644 index 00000000..433083fb --- /dev/null +++ b/modules/core/src/api/infrastructure/renderers/index.ts @@ -0,0 +1,3 @@ +export * from "./fast-report"; +export * from "./handlebars"; +export * from "./renderer-template-resolver"; diff --git a/modules/core/src/api/infrastructure/templates/template-resolver.ts b/modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts similarity index 73% rename from modules/core/src/api/infrastructure/templates/template-resolver.ts rename to modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts index cc64b78f..077a4461 100644 --- a/modules/core/src/api/infrastructure/templates/template-resolver.ts +++ b/modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts @@ -1,21 +1,12 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -export interface ITemplateResolver { - /** Devuelve la ruta absoluta del fichero de plantilla */ - resolveTemplatePath(module: string, companySlug: string, templateName: string): string; - - /** Compila el contenido de la plantilla (string) */ - compile(templateSource: string): unknown; - - /** Localiza y compila el template */ - compileTemplate(module: string, companySlug: string, templateName: string): unknown; -} +import type { IRendererTemplateResolver } from "../../application"; /** * Resuelve rutas de plantillas para desarrollo y producción. */ -export abstract class TemplateResolver implements ITemplateResolver { +export abstract class RendererTemplateResolver implements IRendererTemplateResolver { constructor(protected readonly rootPath: string) {} /** Une partes de ruta relativas al rootPath */ @@ -65,8 +56,4 @@ export abstract class TemplateResolver implements ITemplateResolver { protected readTemplateFile(templatePath: string): string { return readFileSync(templatePath, "utf8"); } - - abstract compile(templateSource: string): unknown; - - abstract compileTemplate(module: string, companySlug: string, templateName: string): unknown; } diff --git a/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts b/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts new file mode 100644 index 00000000..0760b5b9 --- /dev/null +++ b/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts @@ -0,0 +1,40 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { buildSafePath } from "@repo/rdx-utils"; + +import type { ReportStorage, ReportStorageKey } from "./report-storage"; + +export class FileSystemReportStorage implements ReportStorage { + public constructor(private readonly basePath: string) {} + + public async exists(docKey: ReportStorageKey): Promise { + try { + await fs.access(this.resolvePath(docKey)); + return true; + } catch { + return false; + } + } + + public async read(docKey: ReportStorageKey): Promise { + const filePath = this.resolvePath(docKey); + return docKey.format === "PDF" ? fs.readFile(filePath) : fs.readFile(filePath, "utf-8"); + } + + public async write(docKey: ReportStorageKey, payload: Buffer | string): Promise { + const filePath = this.resolvePath(docKey); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, payload); + } + + private resolvePath(key: ReportStorageKey): string { + const ext = key.format.toLowerCase(); + + return buildSafePath({ + basePath: this.basePath, + segments: [key.documentType.toLowerCase()], + filename: `${key.documentId}.${ext}`, + }); + } +} diff --git a/modules/core/src/api/infrastructure/reporting/index.ts b/modules/core/src/api/infrastructure/reporting/index.ts new file mode 100644 index 00000000..b90fe6c7 --- /dev/null +++ b/modules/core/src/api/infrastructure/reporting/index.ts @@ -0,0 +1,2 @@ +export * from "./filesystem-report-storage"; +export * from "./report-storage"; diff --git a/modules/core/src/api/infrastructure/reporting/report-storage.ts b/modules/core/src/api/infrastructure/reporting/report-storage.ts new file mode 100644 index 00000000..197f86f5 --- /dev/null +++ b/modules/core/src/api/infrastructure/reporting/report-storage.ts @@ -0,0 +1,11 @@ +export type ReportStorageKey = { + documentType: string; + documentId: string; + format: "PDF" | "HTML"; +}; + +export interface ReportStorage { + exists(docKey: ReportStorageKey): Promise; + read(docKey: ReportStorageKey): Promise; + write(docKey: ReportStorageKey, payload: Buffer | string): Promise; +} diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts index d39197ab..02262afa 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -7,12 +7,12 @@ import { logger } from "../logger"; export class SequelizeTransactionManager extends TransactionManager { protected _database: Sequelize | null = null; - private readonly isolationLevel: string; + private isolationLevel: Transaction.ISOLATION_LEVELS; protected async _startTransaction(): Promise { // Sequelize adquiere una conexión del pool y la asocia a la transacción. return await this._database!.transaction({ - isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED, + isolationLevel: this.isolationLevel, }); } @@ -26,7 +26,7 @@ export class SequelizeTransactionManager extends TransactionManager { throw new InfrastructureError("[SequelizeTransactionManager] Database not available"); } - constructor(database: Sequelize, options?: { isolationLevel?: string }) { + constructor(database: Sequelize, options?: { isolationLevel?: Transaction.ISOLATION_LEVELS }) { super(); this._database = database; this.isolationLevel = options?.isolationLevel ?? Transaction.ISOLATION_LEVELS.READ_COMMITTED; diff --git a/modules/core/src/common/dto/amount-money.dto.ts b/modules/core/src/common/dto/amount-money.dto.ts index ec4c8dc9..e914a700 100644 --- a/modules/core/src/common/dto/amount-money.dto.ts +++ b/modules/core/src/common/dto/amount-money.dto.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; + import { AmountBaseSchema } from "./base.schemas"; /** diff --git a/modules/core/src/common/dto/list-view.response.dto.ts b/modules/core/src/common/dto/list-view.response.dto.ts index 7bcc4e7a..75572f7b 100644 --- a/modules/core/src/common/dto/list-view.response.dto.ts +++ b/modules/core/src/common/dto/list-view.response.dto.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; + import { MetadataSchema } from "./metadata.dto"; /** diff --git a/modules/core/src/common/dto/percentage.dto.ts b/modules/core/src/common/dto/percentage.dto.ts index 5984b53c..65df9f39 100644 --- a/modules/core/src/common/dto/percentage.dto.ts +++ b/modules/core/src/common/dto/percentage.dto.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; + import { NumericStringSchema } from "./base.schemas"; /** diff --git a/modules/core/src/common/dto/quantity.dto.ts b/modules/core/src/common/dto/quantity.dto.ts index 85942d2c..e7a3a883 100644 --- a/modules/core/src/common/dto/quantity.dto.ts +++ b/modules/core/src/common/dto/quantity.dto.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; + import { NumericStringSchema } from "./base.schemas"; /** diff --git a/modules/core/src/common/dto/tax-type.dto.ts b/modules/core/src/common/dto/tax-type.dto.ts index cbaad322..08e96796 100644 --- a/modules/core/src/common/dto/tax-type.dto.ts +++ b/modules/core/src/common/dto/tax-type.dto.ts @@ -1,4 +1,4 @@ -import { PercentageDTO } from "./percentage.dto"; +import type { PercentageDTO } from "./percentage.dto"; export interface ITaxTypeDTO { id: string; diff --git a/modules/core/src/common/types/dto.d.ts b/modules/core/src/common/types/dto.d.ts new file mode 100644 index 00000000..cad06e4f --- /dev/null +++ b/modules/core/src/common/types/dto.d.ts @@ -0,0 +1,2 @@ +// biome-ignore lint/style/useNamingConvention: false positive +export type DTO> = T; diff --git a/modules/core/src/common/types/index.ts b/modules/core/src/common/types/index.ts index b38c7d74..f36c9945 100644 --- a/modules/core/src/common/types/index.ts +++ b/modules/core/src/common/types/index.ts @@ -1 +1,2 @@ +export * from "./dto.d"; export * from "./module-metadata.d"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts index 91586897..aaf29b3b 100644 --- a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts @@ -50,7 +50,7 @@ export class IssuedInvoiceTaxesReportPresenter extends Presenter { + return taxes?.map((item, _index) => { return this._mapTax(item); }); } diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts index e6336958..f0734608 100644 --- a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice.report.presenter.ts @@ -21,13 +21,13 @@ export class IssuedInvoiceReportPresenter extends Presenter< const itemsPresenter = this.presenterRegistry.getPresenter({ resource: "issued-invoice-items", projection: "REPORT", - format: "JSON", + format: "DTO", }); const taxesPresenter = this.presenterRegistry.getPresenter({ resource: "issued-invoice-taxes", projection: "REPORT", - format: "JSON", + format: "DTO", }); const locale = issuedInvoiceDTO.language_code; @@ -49,6 +49,11 @@ export class IssuedInvoiceReportPresenter extends Presenter< taxes: taxesDTO, items: itemsDTO, + recipient: { + ...issuedInvoiceDTO.recipient, + format_address: this.formatAddress(issuedInvoiceDTO.recipient), + }, + invoice_date: DateHelper.format(issuedInvoiceDTO.invoice_date, locale), subtotal_amount: MoneyDTOHelper.format( issuedInvoiceDTO.subtotal_amount, @@ -70,6 +75,43 @@ export class IssuedInvoiceReportPresenter extends Presenter< total_amount: MoneyDTOHelper.format(issuedInvoiceDTO.total_amount, locale, moneyOptions), payment_method: this._formatPaymentMethodDTO(issuedInvoiceDTO.payment_method), + + verifactu: { + ...issuedInvoiceDTO.verifactu, + qr_code: issuedInvoiceDTO.verifactu.qr_code.replace("data:image/png;base64,", ""), + }, }; } + + protected formatAddress(recipient: GetIssuedInvoiceByIdResponseDTO["recipient"]): string { + const lines: string[] = []; + + // Líneas de calle + if (recipient.street) { + lines.push(recipient.street); + } + + if (recipient.street2) { + lines.push(recipient.street2); + } + + // Ciudad + código postal + const cityLine = [recipient.postal_code, recipient.city].filter(Boolean).join(" "); + + if (cityLine) { + lines.push(cityLine); + } + + // Provincia + if (recipient.province) { + lines.push(recipient.province); + } + + // País + if (recipient.country) { + lines.push(recipient.country); + } + + return lines.join("\n"); + } } 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 44c76eca..2214bed3 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 @@ -1,23 +1,29 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; +import type { DTO } from "@erp/core"; +import type { + IPresenterRegistry, + IRendererRegistry, + ITransactionManager, + RendererFormat, +} from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; +import type { CustomerInvoice } from "../../../../domain"; import type { CustomerInvoiceApplicationService } from "../../../services"; -import type { IssuedInvoiceReportPDFPresenter } from "./reporter/issued-invoice.report.pdf"; - type ReportIssuedInvoiceUseCaseInput = { companyId: UniqueID; companySlug: string; invoice_id: string; - format: "pdf" | "html"; + format: RendererFormat; }; export class ReportIssuedInvoiceUseCase { constructor( private readonly service: CustomerInvoiceApplicationService, private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry + private readonly presenterRegistry: IPresenterRegistry, + private readonly rendererRegistry: IRendererRegistry ) {} public async execute(params: ReportIssuedInvoiceUseCaseInput) { @@ -30,11 +36,21 @@ export class ReportIssuedInvoiceUseCase { } const invoiceId = idOrError.data; - const pdfPresenter = this.presenterRegistry.getPresenter({ + + const fullPresenter = this.presenterRegistry.getPresenter({ + resource: "issued-invoice", + projection: "FULL", + }); + + const reportPresenter = this.presenterRegistry.getPresenter({ resource: "issued-invoice", projection: "REPORT", + }); + + const renderer = this.rendererRegistry.getRenderer({ + resource: "issued-invoice", format, - }) as IssuedInvoiceReportPDFPresenter; + }); return this.transactionManager.complete(async (transaction) => { try { @@ -49,18 +65,33 @@ export class ReportIssuedInvoiceUseCase { } const invoice = invoiceOrError.data; - const reportData = await pdfPresenter.toOutput(invoice, { companySlug }); + const documentId = `${invoice.series.getOrUndefined()}${invoice.invoiceNumber.toString()}`; - if (format === "html") { + const invoiceDTO = await fullPresenter.toOutput(invoice, { companySlug }); + + const reportInvoiceDTO = await reportPresenter.toOutput(invoiceDTO, { companySlug }); + + const result = (await renderer.render(reportInvoiceDTO, { + companySlug, + documentId, + })) as unknown; + + if (result.isFailure) { + return Result.fail(result.error); + } + + const { payload: invoiceRendered } = result.data; + + if (format === "HTML") { return Result.ok({ - data: String(reportData), + data: String(invoiceRendered), filename: undefined, }); } return Result.ok({ - data: reportData as Buffer, - filename: `proforma-${invoice.invoiceNumber}.pdf`, + data: invoiceRendered as Buffer, + filename: `customer-invoice-${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/index.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts deleted file mode 100644 index 57b3a2da..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./issued-invoice.report.html"; -export * from "./issued-invoice.report.pdf"; diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts deleted file mode 100644 index a4d13099..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TemplatePresenter } from "@erp/core/api"; - -import type { CustomerInvoice } from "../../../../../domain"; -import type { - IssuedInvoiceFullPresenter, - IssuedInvoiceReportPresenter, -} from "../../../../presenters"; - -export class IssuedInvoiceReportHTMLPresenter extends TemplatePresenter { - toOutput(invoice: CustomerInvoice, params: { companySlug: string }): string { - const { companySlug } = params; - const dtoPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "FULL", - }) as IssuedInvoiceFullPresenter; - - const prePresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "REPORT", - format: "JSON", - }) as IssuedInvoiceReportPresenter; - - const invoiceDTO = dtoPresenter.toOutput(invoice); - const prettyDTO = prePresenter.toOutput(invoiceDTO); - - // Obtener y compilar la plantilla HTML - const template = this.templateResolver.compileTemplate( - "customer-invoices", - companySlug, - "issued-invoice.hbs" - ); - - return template(prettyDTO); - } -} 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 deleted file mode 100644 index 9f04f993..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Presenter } from "@erp/core/api"; -import puppeteer from "puppeteer"; - -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, - Promise> -> { - async toOutput( - invoice: CustomerInvoice, - params: { companySlug: string } - ): Promise> { - try { - const htmlPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "REPORT", - format: "HTML", - }) as IssuedInvoiceReportHTMLPresenter; - - const htmlData = htmlPresenter.toOutput(invoice, params); - - // Generar el PDF con Puppeteer - const browser = await puppeteer.launch({ - headless: true, - args: [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--disable-extensions", - "--font-render-hinting=medium", - ], - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - }); - - const page = await browser.newPage(); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(60000); - - await page.setContent(htmlData, { - waitUntil: "networkidle2", - }); - - // Espera extra opcional si hay imágenes base64 muy grandes - await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); - - const reportPDF = await page.pdf({ - format: "A4", - margin: { - top: 0, - left: 0, - right: 0, - bottom: 0, - }, - landscape: false, - preferCSSPageSize: true, - omitBackground: false, - printBackground: true, - displayHeaderFooter: true, - headerTemplate: "
", - footerTemplate: - '
Página de
', - }); - - await browser.close(); - - return Buffer.from(reportPDF); - } catch (err: unknown) { - console.error(err); - throw err as Error; - } - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts index 42872fb1..e69de29b 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts @@ -1,2 +0,0 @@ -export * from "./proforma.report.html"; -export * from "./proforma.report.pdf"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_acana.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_acana.jpg deleted file mode 100644 index c21c8b29..00000000 Binary files a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_acana.jpg and /dev/null differ diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_rodax.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_rodax.jpg deleted file mode 100755 index 75df86fa..00000000 Binary files a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/logo_rodax.jpg and /dev/null differ diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs deleted file mode 100644 index c0e2b6b8..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - Factura F26200 - - - - -
- - -
-
- - -
-

Aliso Design S.L. B86913910

-

C/ La Fundición, 27. Pol. Santa Ana

-

Rivas Vaciamadrid 28522 Madrid

-

Telf: 91 301 65 57 / 91 301 65 58

-

- info@acanainteriorismo.com - - www.acanainteriorismo.com -

-
-
- -
- Factura -
-
- - -
- -
-

Factura nº: {{series}}{{invoice_number}}

-

Fecha: {{invoice_date}}

-

Página de

-
- -
-

{{recipient.name}}

-

{{recipient.tin}}

-

{{recipient.street}}

-

{{recipient.postal_code}} {{recipient.city}} {{recipient.province}}

-
- -
- -
- -
-
- - - - - - - - - - - - - - - - {{#each items}} - - - - - - - - - {{/each}} - - - - - - {{#if discount_percentage}} - - - {{else}} - - - {{/if}} - - - {{#if discount_percentage}} - - - - - - - - - {{/if}} - - {{#each taxes}} - - - - - {{/each}} - - - - - - -
ConceptoUd.Imp. Imp. total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if discount_percentage}}{{discount_percentage}}{{else}} {{/if}}{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}
- {{#if payment_method}} -

Forma de pago: {{payment_method}}

- {{/if}} - - {{#if notes}} -

Notas: {{notes}}

- {{/if}} -
Importe neto{{subtotal_amount}}Base imponible{{taxable_amount}}
Dto {{discount_percentage}}{{discount_amount.value}}
Base imponible{{taxable_amount}}
{{tax_name}}{{taxes_amount}}
Total factura{{total_amount}}
-
-
- - -
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs_BAK b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs_BAK deleted file mode 100644 index a14b7fe9..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template.hbs_BAK +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - Presupuesto #{{id}} - - - - - - -
-
- - - - - - - - - - - - - {{#each items}} - - - - - - - - - {{/each}} - -
Cant.DescripciónPrec. UnitarioSubtotalDto (%)Importe total
{{quantity}}{{description}}{{unit_price}}{{subtotal_price}}{{discount}}{{total_price}}
-
- -
- -
-
-

Forma de pago: {{payment_method}}

-
-
-

Notas: {{notes}}

-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Importe neto{{subtotal_price}}
% Descuento{{discount.amount}}{{discount_price}}
Base imponible{{before_tax_price}}
% IVA{{tax}}{{tax_price}}
Importe total{{total_price}}
-
-
- -
-
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_proforma.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_proforma.hbs deleted file mode 100644 index b5d4df16..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_proforma.hbs +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - Factura F26200 - - - - - - - -
-

FACTURA PROFORMA

-
-
- -
- -
- - TOTAL: {{total_amount}} -
-
-
- - - - - - - - - - - - - - {{#each items}} - - - - - - - - {{/each}} - -
ConceptoCantidadPrecio unidadImporte total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if subtotal_amount}}{{subtotal_amount}}{{else}} {{/if}}
-
- -
- -
- {{#if payment_method}} -
-

Forma de pago: {{payment_method}}

-
- {{else}} - - {{/if}} - {{#if notes}} -
-

Notas: {{notes}}

-
- {{else}} - - {{/if}} - -
- -
- - - {{#if discount_percentage}} - - - - - - - - - - - {{else}} - - {{/if}} - - - - - - - - - - - - - - - - -
Importe neto {{subtotal_amount}}
Descuento {{discount_percentage}} {{discount_amount.value}}
Base imponible {{taxable_amount}}
IVA 21% {{taxes_amount}}
- Total factura -   - {{total_amount}}
-
-
-
- -
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_rodax.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_rodax.hbs deleted file mode 100644 index c31ea407..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/template_rodax.hbs +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - Factura F26200 - - - - - -
- -
- -
-
- -
- -
- -
- - TOTAL: {{total_amount}} -
-
-
- - - - - - - - - - - - - - {{#each items}} - - - - - - - - {{/each}} - -
ConceptoCantidadPrecio unidadImporte total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}
-
- -
- -
- {{#if payment_method}} -
-

Forma de pago: {{payment_method}}

-
- {{else}} - - {{/if}} - {{#if notes}} -
-

Notas: {{notes}}

-
- {{else}} - - {{/if}} - -
- -
- - - {{#if discount_percentage}} - - - - - - - - - - - {{else}} - - {{/if}} - - - - - - {{#each taxes}} - - - - - - {{/each}} - - - - - - -
Importe neto {{subtotal_amount}}
Descuento {{discount_percentage}} {{discount_amount.value}}
Base imponible {{taxable_amount}}
{{tax_name}} {{taxes_amount}}
- Total factura -   - {{total_amount}}
-
-
-
- - -
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-footer-logos.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-footer-logos.jpg deleted file mode 100644 index dbaaf5ea..00000000 Binary files a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-footer-logos.jpg and /dev/null differ diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-logo.svg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-logo.svg deleted file mode 100644 index c5624526..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/proforma/uecko-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts index 027b5c6a..6b2aeaf0 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { GetIssuedInvoiceUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +14,11 @@ export class GetIssueInvoiceController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts index 8cd49683..b1faa7f8 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; import type { ListIssuedInvoicesUseCase } from "../../../../application"; @@ -10,7 +15,11 @@ export class ListIssuedInvoicesController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } private getCriteriaWithDefaultOrder() { 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 19972d66..8a16da80 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 @@ -1,4 +1,11 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + type RendererFormat, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; +import type { ReportIssueInvoiceByIdQueryRequestDTO } from "@erp/customer-invoices/common"; import type { ReportIssuedInvoiceUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +16,11 @@ export class ReportIssuedInvoiceController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { @@ -20,13 +31,26 @@ 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 { format } = this.req.query as ReportIssueInvoiceByIdQueryRequestDTO; - const result = await this.useCase.execute({ invoice_id, companyId, companySlug, format }); + const result = await this.useCase.execute({ + invoice_id, + companyId, + companySlug, + format: format as RendererFormat, + }); return result.match( - ({ data, filename }) => - filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string), + ({ data, filename }) => { + if (format === "PDF") { + return this.downloadPDF(data as Buffer, String(filename)); + } + if (format === "HTML") { + return this.downloadHTML(data as string); + } + // JSON + return this.json(data); + }, (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts index 5de1e80a..b0ed8ded 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto"; import type { ChangeStatusProformaUseCase } from "../../../../application"; @@ -10,7 +15,11 @@ export class ChangeStatusProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts index 090935b0..8566b702 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { CreateProformaRequestDTO } from "../../../../../common/dto"; import type { CreateProformaUseCase } from "../../../../application"; @@ -10,7 +15,11 @@ export class CreateProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts index f92f3a0c..49d9bfd0 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { DeleteProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +14,11 @@ export class DeleteProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts index 5d431079..75d49ff3 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { GetProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +14,11 @@ export class GetProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts index 667f8744..1c219955 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { IssueProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +14,11 @@ export class IssueProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts index 38157072..c0925f08 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; import type { ListProformasUseCase } from "../../../../application"; @@ -10,7 +15,11 @@ export class ListProformasController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } private getCriteriaWithDefaultOrder() { 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 822f09c2..0f07181b 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 @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { ReportProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; @@ -9,7 +14,11 @@ export class ReportProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts index eb25ea71..c939f506 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts @@ -1,4 +1,9 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; import type { UpdateProformaUseCase } from "../../../../application"; @@ -10,7 +15,11 @@ export class UpdateProformaController extends ExpressController { this.errorMapper = customerInvoicesApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { 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 9884a32c..d6a37f3f 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 @@ -1,5 +1,5 @@ -import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; -import { type ModuleParams, validateRequest } from "@erp/core/api"; +import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; +import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import type { ILogger } from "@repo/rdx-logger"; import { type Application, type NextFunction, type Request, type Response, Router } from "express"; import type { Sequelize } from "sequelize"; @@ -39,10 +39,10 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { router.use([ (req: Request, res: Response, next: NextFunction) => - enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireAuthenticated()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas (req: Request, res: Response, next: NextFunction) => - enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireCompanyContext()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas ]); // ---------------------------------------------- @@ -74,6 +74,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { //checkTabContext, 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 7ba284bc..3cca5b32 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts @@ -1,5 +1,5 @@ -import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; -import { type ModuleParams, validateRequest } from "@erp/core/api"; +import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; +import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import type { ILogger } from "@repo/rdx-logger"; import { type Application, type NextFunction, type Request, type Response, Router } from "express"; import type { Sequelize } from "sequelize"; @@ -32,11 +32,11 @@ import { export const proformasRouter = (params: ModuleParams) => { const { app, baseRoutePath, logger } = params as { + env: Record; app: Application; database: Sequelize; baseRoutePath: string; logger: ILogger; - templateRootPath: string; }; const deps = buildProformasDependencies(params); @@ -52,10 +52,10 @@ export const proformasRouter = (params: ModuleParams) => { router.use([ (req: Request, res: Response, next: NextFunction) => - enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireAuthenticated()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas (req: Request, res: Response, next: NextFunction) => - enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireCompanyContext()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas ]); // ---------------------------------------------- diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts index 1f5039e7..169df1d3 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts @@ -4,13 +4,17 @@ import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core" import type { IMapperRegistry, IPresenterRegistry, - ITemplateResolver, + IRendererRegistry, ModuleParams, } from "@erp/core/api"; import { - HandlebarsTemplateResolver, + FastReportExecutableResolver, + FastReportProcessRunner, + FastReportTemplateResolver, + FileSystemReportStorage, InMemoryMapperRegistry, InMemoryPresenterRegistry, + InMemoryRendererRegistry, SequelizeTransactionManager, } from "@erp/core/api"; @@ -28,12 +32,9 @@ import { ListIssuedInvoicesUseCase, ReportIssuedInvoiceUseCase, } from "../application"; -import { - IssuedInvoiceReportHTMLPresenter, - IssuedInvoiceReportPDFPresenter, -} from "../application/use-cases/issued-invoices/report-issued-invoices/reporter"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; +import { IssuedInvoiceReportPDFRenderer } from "./renderers/issued-invoice-report-pdf.renderer"; import { CustomerInvoiceRepository } from "./sequelize"; import { SequelizeInvoiceNumberGenerator } from "./services"; @@ -41,9 +42,9 @@ export type IssuedInvoicesDeps = { transactionManager: SequelizeTransactionManager; mapperRegistry: IMapperRegistry; presenterRegistry: IPresenterRegistry; + rendererRegistry: IRendererRegistry; repo: CustomerInvoiceRepository; appService: CustomerInvoiceApplicationService; - templateResolver: ITemplateResolver; catalogs: { taxes: JsonTaxCatalogProvider; }; @@ -55,13 +56,14 @@ export type IssuedInvoicesDeps = { }; export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesDeps { - const { database, templateRootPath } = params; + const { database, env } = params; + const templateRootPath = env.TEMPLATES_PATH; + const documentRootPath = env.DOCUMENTS_PATH; /** Dominio */ const catalogs = { taxes: SpainTaxCatalogProvider() }; /** Infraestructura */ - const templateResolver = new HandlebarsTemplateResolver(templateRootPath); const transactionManager = new SequelizeTransactionManager(database); const mapperRegistry = new InMemoryMapperRegistry(); @@ -81,6 +83,25 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv const repository = new CustomerInvoiceRepository({ mapperRegistry, database }); const numberGenerator = new SequelizeInvoiceNumberGenerator(); + // Renderers Registry + const frExecResolver = new FastReportExecutableResolver(); + const frProcessRunner = new FastReportProcessRunner(); + const frTemplateResolver = new FastReportTemplateResolver(templateRootPath); + + const rendererRegistry = new InMemoryRendererRegistry(); + const reportStorage = new FileSystemReportStorage(documentRootPath); + rendererRegistry.registerRenderers([ + { + key: { resource: "issued-invoice", format: "PDF" }, + renderer: new IssuedInvoiceReportPDFRenderer( + frExecResolver, + frProcessRunner, + frTemplateResolver, + reportStorage + ), + }, + ]); + /** Aplicación */ const appService = new CustomerInvoiceApplicationService(repository, numberGenerator); @@ -113,25 +134,17 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv // REPORT { - key: { resource: "issued-invoice", projection: "REPORT", format: "JSON" }, + key: { resource: "issued-invoice", projection: "REPORT", format: "DTO" }, presenter: new IssuedInvoiceReportPresenter(presenterRegistry), }, { - key: { resource: "issued-invoice-taxes", projection: "REPORT", format: "JSON" }, + key: { resource: "issued-invoice-taxes", projection: "REPORT", format: "DTO" }, presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), }, { - key: { resource: "issued-invoice-items", projection: "REPORT", format: "JSON" }, + key: { resource: "issued-invoice-items", projection: "REPORT", format: "DTO" }, presenter: new IssuedInvoiceItemsReportPresenter(presenterRegistry), }, - { - key: { resource: "issued-invoice", projection: "REPORT", format: "HTML" }, - presenter: new IssuedInvoiceReportHTMLPresenter(presenterRegistry, templateResolver), - }, - { - key: { resource: "issued-invoice", projection: "REPORT", format: "PDF" }, - presenter: new IssuedInvoiceReportPDFPresenter(presenterRegistry), - }, ]); const useCases: IssuedInvoicesDeps["useCases"] = { @@ -141,7 +154,12 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv get_issued_invoice: () => new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), report_issued_invoice: () => - new ReportIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), + new ReportIssuedInvoiceUseCase( + appService, + transactionManager, + presenterRegistry, + rendererRegistry + ), }; return { @@ -149,9 +167,9 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv repo: repository, mapperRegistry, presenterRegistry, + rendererRegistry, appService, catalogs, - templateResolver, useCases, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts b/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts index f6c758de..cffd9e73 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts @@ -5,7 +5,6 @@ import { HandlebarsTemplateResolver, type IMapperRegistry, type IPresenterRegistry, - type ITemplateResolver, InMemoryMapperRegistry, InMemoryPresenterRegistry, type ModuleParams, @@ -22,8 +21,6 @@ import { ListProformasUseCase, ProformaFullPresenter, ProformaListPresenter, - ProformaReportHTMLPresenter, - ProformaReportPDFPresenter, ReportProformaUseCase, UpdateProformaUseCase, } from "../application"; @@ -48,7 +45,6 @@ export type ProformasDeps = { catalogs: { taxes: JsonTaxCatalogProvider; }; - templateResolver: ITemplateResolver; useCases: { list_proformas: () => ListProformasUseCase; get_proforma: () => GetProformaUseCase; @@ -62,7 +58,8 @@ export type ProformasDeps = { }; export function buildProformasDependencies(params: ModuleParams): ProformasDeps { - const { database, templateRootPath } = params; + const { database, env } = params; + const templateRootPath = env.TEMPLATES_PATH; /** Dominio */ const catalogs = { taxes: SpainTaxCatalogProvider() }; @@ -116,25 +113,17 @@ export function buildProformasDependencies(params: ModuleParams): ProformasDeps // REPORT { - key: { resource: "proforma", projection: "REPORT", format: "JSON" }, + key: { resource: "proforma", projection: "REPORT" }, presenter: new ProformaReportPresenter(presenterRegistry), }, { - key: { resource: "proforma-taxes", projection: "REPORT", format: "JSON" }, + key: { resource: "proforma-taxes", projection: "REPORT" }, presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), }, { - key: { resource: "proforma-items", projection: "REPORT", format: "JSON" }, + key: { resource: "proforma-items", projection: "REPORT" }, presenter: new ProformaItemsReportPresenter(presenterRegistry), }, - { - key: { resource: "proforma", projection: "REPORT", format: "HTML" }, - presenter: new ProformaReportHTMLPresenter(presenterRegistry, templateResolver), - }, - { - key: { resource: "proforma", projection: "REPORT", format: "PDF" }, - presenter: new ProformaReportPDFPresenter(presenterRegistry), - }, ]); const useCases: ProformasDeps["useCases"] = { @@ -160,7 +149,6 @@ export function buildProformasDependencies(params: ModuleParams): ProformasDeps mapperRegistry, presenterRegistry, appService, - templateResolver, catalogs, useCases, }; diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/index.ts b/modules/customer-invoices/src/api/infrastructure/renderers/index.ts new file mode 100644 index 00000000..91aadb66 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/renderers/index.ts @@ -0,0 +1,3 @@ +export * from "./issued-invoice.renderer.html"; +export * from "./issued-invoice.renderer.json"; +export * from "./issued-invoice-report-pdf.renderer"; diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts new file mode 100644 index 00000000..b3def1d7 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts @@ -0,0 +1,36 @@ +import type { DTO } from "@erp/core"; +import { FastReportRenderer } from "@erp/core/api"; + +export type IssuedInvoiceReportPDFRendererParams = { + companySlug: string; + documentId: string; +}; + +export class IssuedInvoiceReportPDFRenderer extends FastReportRenderer { + protected readonly templateName = "issued-invoice.frx"; + protected companySlug!: string; + + render(source: DTO, params: IssuedInvoiceReportPDFRendererParams) { + this.companySlug = params.companySlug; + + return this.renderInternal({ + inputData: source, + format: "PDF", + storageKey: { + documentType: "customer-invoice", + documentId: params.documentId, + format: "PDF", + }, + }); + } + + protected resolveTemplatePath(): string { + const templatePath = this.templateResolver.resolveTemplatePath( + "customer-invoices", + this.companySlug, + this.templateName + ); + + return templatePath; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts new file mode 100644 index 00000000..ccacaa3f --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts @@ -0,0 +1,28 @@ +import { TemplateRenderer } from "@erp/core/api"; + +import type { CustomerInvoice } from "../../domain"; + +import type { IssuedInvoiceReportJSONRenderer } from "./issued-invoice.renderer.json"; + +export class IssuedInvoiceReportHTMLRenderer extends TemplateRenderer { + toOutput(invoice: CustomerInvoice, params: { companySlug: string }): string { + const { companySlug } = params; + + const jsonRenderer = this.presenterRegistry.getRenderer({ + resource: "issued-invoice", + projection: "REPORT", + format: "JSON", + }) as IssuedInvoiceReportJSONRenderer; + + const jsonData = jsonRenderer.toOutput(invoice, params); + + // Obtener y compilar la plantilla HTML + const template = this.templateResolver.compileTemplate( + "customer-invoices", + companySlug, + "issued-invoice.hbs" + ); + + return template(jsonData); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts new file mode 100644 index 00000000..042f9ae5 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts @@ -0,0 +1,31 @@ +import { Renderer } from "@erp/core/api"; + +import type { + IssuedInvoiceFullRenderer, + IssuedInvoiceReportRenderer, +} from "../../application/presenters"; +import type { CustomerInvoice } from "../../domain"; + +export class IssuedInvoiceReportJSONRenderer extends Renderer< + CustomerInvoice, + Record +> { + toOutput(invoice: CustomerInvoice, params: { companySlug: string }): Record { + const dtoRenderer = this.presenterRegistry.getRenderer({ + resource: "issued-invoice", + projection: "FULL", + format: "DTO", + }) as IssuedInvoiceFullRenderer; + + const preRenderer = this.presenterRegistry.getRenderer({ + resource: "issued-invoice", + projection: "REPORT", + format: "DTO", + }) as IssuedInvoiceReportRenderer; + + const invoiceDTO = dtoRenderer.toOutput(invoice); + const prettyDTO = preRenderer.toOutput(invoiceDTO); + + return prettyDTO; + } +} 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 3ace8985..97af5c3d 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 @@ -9,7 +9,12 @@ export type ReportIssueInvoiceByIdParamsRequestDTO = z.infer< >; export const ReportIssueInvoiceByIdQueryRequestSchema = 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 ReportIssueInvoiceByIdQueryRequestDTO = z.infer< diff --git a/modules/customer-invoices/templates/rodax/issued-invoice.frx b/modules/customer-invoices/templates/rodax/issued-invoice.frx new file mode 100644 index 00000000..7b417bc5 --- /dev/null +++ b/modules/customer-invoices/templates/rodax/issued-invoice.frx @@ -0,0 +1,229 @@ + + + using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Forms; +using System.Drawing; +using System.Data; +using FastReport; +using FastReport.Data; +using FastReport.Dialog; +using FastReport.Barcode; +using FastReport.Table; +using FastReport.Utils; + +namespace FastReport +{ + public class ReportScript + { + + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/customer-invoices/templates/rodax/issued-invoice.hbs b/modules/customer-invoices/templates/rodax/issued-invoice.hbs deleted file mode 100644 index 9b9b08cb..00000000 --- a/modules/customer-invoices/templates/rodax/issued-invoice.hbs +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - - Factura F26200 - - - - - -
-
-
- Logo Rodax -
-
-

Factura nº: {{series}}{{invoice_number}}

-

Fecha: {{invoice_date}}

-
-
-

{{recipient.name}}

-

{{recipient.tin}}

-

{{recipient.street}}

-

{{recipient.postal_code}}  {{recipient.city}}  {{recipient.province}}

-
-
-
-
- - -
-
- Factura -
-
-

Telf: 91 785 02 47 / 686 62 10 59

-

info@rodax-software.com

-

www.rodax-software.com

-
- {{#if verifactu.qr_code}} -
-
- QR tributario factura verificable en sede electronica de AEAT VERI*FACTU -
-
- QR factura -
-
- {{/if}} -
- -
- -
-
- -
- -
-
- - TOTAL: {{total_amount}} -
-
-
- - - - - - - - - - - - - - {{#each items}} - - - - - - - - {{/each}} - -
ConceptoCantidadPrecio unidadImporte total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}
-
- -
- -
- {{#if payment_method}} -
-

Forma de pago: {{payment_method}}

-
- {{else}} - - {{/if}} - {{#if notes}} -
-

Notas: {{notes}}

-
- {{else}} - - {{/if}} - -
- -
- - - {{#if discount_percentage}} - - - - - - - - - - - {{else}} - - {{/if}} - - - - - - {{#each taxes}} - - - - - - {{/each}} - - - - - - -
Importe neto {{subtotal_amount}}
Descuento {{discount_percentage}} {{discount_amount.value}}
Base imponible {{taxable_amount}}
{{tax_name}} {{taxes_amount}}
- Total factura -   - {{total_amount}}
-
-
-
- - -
- -
- - - - \ No newline at end of file diff --git a/modules/customer-invoices/templates/rodax/logo2.jpg b/modules/customer-invoices/templates/rodax/logo2.jpg deleted file mode 100644 index 7ab3d183..00000000 Binary files a/modules/customer-invoices/templates/rodax/logo2.jpg and /dev/null differ diff --git a/modules/customer-invoices/templates/rodax/proforma.hbs b/modules/customer-invoices/templates/rodax/proforma.hbs deleted file mode 100644 index c31ea407..00000000 --- a/modules/customer-invoices/templates/rodax/proforma.hbs +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - Factura F26200 - - - - - -
- -
- -
-
- -
- -
- -
- - TOTAL: {{total_amount}} -
-
-
- - - - - - - - - - - - - - {{#each items}} - - - - - - - - {{/each}} - -
ConceptoCantidadPrecio unidadImporte total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}
-
- -
- -
- {{#if payment_method}} -
-

Forma de pago: {{payment_method}}

-
- {{else}} - - {{/if}} - {{#if notes}} -
-

Notas: {{notes}}

-
- {{else}} - - {{/if}} - -
- -
- - - {{#if discount_percentage}} - - - - - - - - - - - {{else}} - - {{/if}} - - - - - - {{#each taxes}} - - - - - - {{/each}} - - - - - - -
Importe neto {{subtotal_amount}}
Descuento {{discount_percentage}} {{discount_amount.value}}
Base imponible {{taxable_amount}}
{{tax_name}} {{taxes_amount}}
- Total factura -   - {{total_amount}}
-
-
-
- - -
- -
- - - - \ No newline at end of file diff --git a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts index 92f26726..3d2d0f41 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts @@ -1,6 +1,12 @@ -import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { CreateCustomerRequestDTO } from "../../../../common/dto"; -import { CreateCustomerUseCase } from "../../../application"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { CreateCustomerRequestDTO } from "../../../../common/dto"; +import type { CreateCustomerUseCase } from "../../../application"; import { customersApiErrorMapper } from "../customer-api-error-mapper"; export class CreateCustomerController extends ExpressController { @@ -9,7 +15,11 @@ export class CreateCustomerController extends ExpressController { this.errorMapper = customersApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts index 26d98cc2..7482ccb6 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts @@ -1,5 +1,11 @@ -import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { DeleteCustomerUseCase } from "../../../application"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { DeleteCustomerUseCase } from "../../../application"; import { customersApiErrorMapper } from "../customer-api-error-mapper"; export class DeleteCustomerController extends ExpressController { @@ -8,7 +14,11 @@ export class DeleteCustomerController extends ExpressController { this.errorMapper = customersApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } async executeImpl(): Promise { diff --git a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts index eb31150b..0ef10557 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts @@ -1,5 +1,11 @@ -import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { GetCustomerUseCase } from "../../../application"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { GetCustomerUseCase } from "../../../application"; import { customersApiErrorMapper } from "../customer-api-error-mapper"; export class GetCustomerController extends ExpressController { @@ -8,7 +14,11 @@ export class GetCustomerController extends ExpressController { this.errorMapper = customersApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts index c4d67210..7e40c5e4 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts @@ -1,6 +1,12 @@ -import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; -import { ListCustomersUseCase } from "../../../application"; + +import type { ListCustomersUseCase } from "../../../application"; import { customersApiErrorMapper } from "../customer-api-error-mapper"; export class ListCustomersController extends ExpressController { @@ -9,7 +15,11 @@ export class ListCustomersController extends ExpressController { this.errorMapper = customersApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } private getCriteriaWithDefaultOrder() { diff --git a/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts index d0bebfc5..bd403d84 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts @@ -1,6 +1,12 @@ -import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; -import { UpdateCustomerUseCase } from "../../../application"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; +import type { UpdateCustomerUseCase } from "../../../application"; import { customersApiErrorMapper } from "../customer-api-error-mapper"; export class UpdateCustomerController extends ExpressController { @@ -9,7 +15,11 @@ export class UpdateCustomerController extends ExpressController { this.errorMapper = customersApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query - this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); } protected async executeImpl() { diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index 314031bd..2ddbcbbe 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -1,4 +1,9 @@ -import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; +import { + type RequestWithAuth, + mockUser, + requireAuthenticated, + requireCompanyContext, +} from "@erp/auth/api"; import { type ModuleParams, validateRequest } from "@erp/core/api"; import type { ILogger } from "@repo/rdx-logger"; import { type Application, type NextFunction, type Request, type Response, Router } from "express"; @@ -43,10 +48,10 @@ export const customersRouter = (params: ModuleParams) => { //router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/); router.use([ (req: Request, res: Response, next: NextFunction) => - enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireAuthenticated()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas (req: Request, res: Response, next: NextFunction) => - enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + requireCompanyContext()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas ]); // ---------------------------------------------- diff --git a/tools/fastreport-cli/FastReportCliGenerator.csproj b/tools/fastreport-cli/FastReportCliGenerator.csproj index 149b9161..9cee2cc7 100644 --- a/tools/fastreport-cli/FastReportCliGenerator.csproj +++ b/tools/fastreport-cli/FastReportCliGenerator.csproj @@ -12,8 +12,8 @@ 1.1 - 1.1.0.8 - 1.1.0.8 + 1.1.0.9 + 1.1.0.9 diff --git a/tools/fastreport-cli/Program.cs b/tools/fastreport-cli/Program.cs index bd78e780..47afef97 100644 --- a/tools/fastreport-cli/Program.cs +++ b/tools/fastreport-cli/Program.cs @@ -104,6 +104,8 @@ class Program Console.WriteLine($"Generated HTML: {outputPath}"); } + report.Dispose(); + return 0; } catch (Exception ex) diff --git a/tools/fastreport-cli/publish/linux/FastReportCliGenerator b/tools/fastreport-cli/publish/linux/FastReportCliGenerator index dd9fa083..0481e244 100755 Binary files a/tools/fastreport-cli/publish/linux/FastReportCliGenerator and b/tools/fastreport-cli/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreport-cli/publish/windows/FastReportCliGenerator.exe b/tools/fastreport-cli/publish/windows/FastReportCliGenerator.exe index 474aa468..d708a866 100755 Binary files a/tools/fastreport-cli/publish/windows/FastReportCliGenerator.exe and b/tools/fastreport-cli/publish/windows/FastReportCliGenerator.exe differ