diff --git a/.vscode/launch.json b/.vscode/launch.json index 0bfe33ae..b2693156 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,5 +1,5 @@ { - "version": "0.3.6", + "version": "0.4.7", "configurations": [ { "name": "WEB: Vite (Chrome)", diff --git a/Dockerfile b/Dockerfile index 68986dc6..dbd18fa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,35 @@ RUN mkdir -p /usr/share/fonts/truetype/barlow \ && fc-cache -f -v +## Configurar locale, timezone y variables de entorno para España + +# Evita prompts interactivos +ENV DEBIAN_FRONTEND=noninteractive + +# Instalar paquetes necesarios +RUN apt-get update && apt-get install -y \ + locales \ + tzdata \ + && rm -rf /var/lib/apt/lists/* + +# Generar locale español de España UTF-8 +RUN sed -i '/es_ES.UTF-8/s/^# //g' /etc/locale.gen \ + && locale-gen + +# Configurar variables de entorno de locale +ENV LANG=es_ES.UTF-8 \ + LANGUAGE=es_ES:es \ + LC_ALL=es_ES.UTF-8 + +# Configurar zona horaria España +ENV TZ=Europe/Madrid +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone + +# Verificación opcional +# RUN locale && date + + # Reducir tamaño de la imagen RUN rm -rf /var/lib/apt/lists/* diff --git a/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts b/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts index 115f4932..cacda792 100644 --- a/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts +++ b/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts @@ -1,3 +1,5 @@ +import { Maybe, Result } from "@repo/rdx-utils"; + import { Account, AccountStatus } from "@/contexts/accounts/domain/"; import { EmailAddress, @@ -11,8 +13,8 @@ import { type MapperParamsType, SequelizeMapper, } from "@/core/common/infrastructure/sequelize/sequelize-mapper"; -import { Maybe, Result } from "@repo/rdx-utils"; -import { AccountCreationAttributes, AccountModel } from "../sequelize/account.model"; + +import type { AccountCreationAttributes, AccountModel } from "../sequelize/account.model"; export interface IAccountMapper extends ISequelizeMapper {} diff --git a/apps/server/package.json b/apps/server/package.json index 0b5f039b..7e73317d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges-server", - "version": "0.3.6", + "version": "0.4.7", "private": true, "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", @@ -38,6 +38,7 @@ "@erp/customer-invoices": "workspace:*", "@erp/customers": "workspace:*", "@repo/rdx-logger": "workspace:*", + "@repo/rdx-utils": "workspace:*", "bcrypt": "^5.1.1", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index f225f3f8..5ae2764c 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -3,12 +3,14 @@ import express, { type Application } from "express"; import helmet from "helmet"; import responseTime from "response-time"; +import type { ConfigType } from "./config"; + // ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config. // dotenv.config(); -import { ENV } from "./config"; + import { logger } from "./lib/logger"; -export function createApp(): Application { +export function createApp(config: ConfigType): Application { const app = express(); // ─────────────────────────────────────────────────────────────────────────── @@ -30,7 +32,7 @@ export function createApp(): Application { }; const prodCors: CorsOptions = { - origin: ENV.FRONTEND_URL, + origin: config.server.frontendUrl, credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], exposedHeaders: [ @@ -44,10 +46,10 @@ export function createApp(): Application { ], }; - app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); - app.options("*", cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); + app.use(cors(config.flags.isDev ? devCors : prodCors)); + app.options("*", cors(config.flags.isDev ? devCors : prodCors)); - app.set("port", process.env.API_PORT ?? 3002); + app.set("port", config.server.port ?? 3002); // Oculta la cabecera x-powered-by app.disable("x-powered-by"); diff --git a/apps/server/src/config/company-certificates.json b/apps/server/src/config/company-certificates.json new file mode 100644 index 00000000..b5c0a975 --- /dev/null +++ b/apps/server/src/config/company-certificates.json @@ -0,0 +1,12 @@ +{ + "acme": { + "certificateId": "acme-prod-cert-v1", + "certificateSecretName": "acme_cert", + "certificatePasswordSecretName": "acme_cert_pwd" + }, + "rodax": { + "certificateId": "no se que poner aqui", + "certificateSecretName": "certificate_secret_name", + "certificatePasswordSecretName": "certificate_password_secret_name" + } +} \ No newline at end of file diff --git a/apps/server/src/config/config-helpers.ts b/apps/server/src/config/config-helpers.ts deleted file mode 100644 index 4f862e80..00000000 --- a/apps/server/src/config/config-helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function asNumber(value: string | undefined, fallback: number): number { - const n = Number(value); - return Number.isFinite(n) ? n : fallback; -} -export function asBoolean(value: string | undefined, fallback: boolean): boolean { - if (value === undefined) return fallback; - return ["1", "true", "yes", "on"].includes(String(value).toLowerCase()); -} -export function required(name: string, value: string | undefined): string { - if (!value) throw new Error(`Missing required environment variable: ${name}`); - return value; -} diff --git a/apps/server/src/config/config.ts b/apps/server/src/config/config.ts new file mode 100644 index 00000000..5315ee5a --- /dev/null +++ b/apps/server/src/config/config.ts @@ -0,0 +1,154 @@ +import type { BaseConfigType } from "@erp/core/api"; +import type { ILogger } from "@repo/rdx-logger"; +import dotenv from "dotenv"; + +import { formatZodErrors } from "./env-error"; +import { EnvSchema } from "./env-schema"; +import { loadCompanyCertificates } from "./load-company-certificates"; +import { logConfigSummary } from "./log-config"; + +dotenv.config(); + +let _config: ConfigType | null = null; + +export type ConfigType = BaseConfigType & { + env: string; + + server: { + port: number; + apiBasePath: string; + frontendUrl: string; + trustProxy: number; + timezone: string; + }; + + database: { + dialect: "mysql" | "postgres" | "sqlite" | "mariadb" | "mssql" | "db2" | "snowflake" | "oracle"; + host: string; + port: number; + name: string; + user: string; + password: string; + logging: boolean; + ssl: boolean; + syncMode: string; + }; + + auth: { + jwt: { + secret: string; + accessExpiration: string; + refreshExpiration: string; + }; + }; + + paths: { + templates: string; + documents: string; + signedDocuments: string; + fastReportBin: string; + }; + + signingService: { + url: string; + method: string; + timeoutMs: number; + maxRetries: number; + }; + + certificates?: Record; + + flags: { + isProd: boolean; + isDev: boolean; + isTest: boolean; + }; +}; + +export const initConfig = (logger: ILogger): ConfigType => { + if (_config) { + return _config; + } + + const parsed = EnvSchema.safeParse(process.env); + + if (!parsed.success) { + const errors = formatZodErrors(parsed.error); + + logger.error("Invalid environment configuration", { + errorCount: errors.length, + errors, + }); + + throw new Error("Invalid environment configuration"); + } + + const env = parsed.data; + + const certificates = loadCompanyCertificates(env.COMPANY_CERTIFICATES_PATH, logger); + + _config = { + env: env.NODE_ENV, + + server: { + port: env.PORT, + apiBasePath: env.API_BASE_PATH, + frontendUrl: env.FRONTEND_URL, + trustProxy: env.TRUST_PROXY, + timezone: env.TIMEZONE, + }, + + database: { + dialect: env.DB_DIALECT, + host: env.DB_HOST, + port: env.DB_PORT, + name: env.DB_NAME, + user: env.DB_USER, + password: env.DB_PASS, + logging: env.DB_LOGGING, + ssl: env.DB_SSL, + syncMode: env.DB_SYNC_MODE, + }, + + auth: { + jwt: { + secret: env.JWT_SECRET, + accessExpiration: env.JWT_ACCESS_EXPIRATION, + refreshExpiration: env.JWT_REFRESH_EXPIRATION, + }, + }, + + paths: { + templates: env.TEMPLATES_PATH, + documents: env.DOCUMENTS_PATH, + signedDocuments: env.SIGNED_DOCUMENTS_PATH, + fastReportBin: env.FASTREPORT_BIN, + }, + + signingService: { + url: env.SIGNING_SERVICE_URL, + method: env.SIGNING_SERVICE_METHOD, + timeoutMs: env.SIGNING_SERVICE_TIMEOUT_MS, + maxRetries: env.SIGNING_SERVICE_MAX_RETRIES, + }, + + certificates, + + flags: { + isProd: env.NODE_ENV === "production", + isDev: env.NODE_ENV === "development", + isTest: env.NODE_ENV === "test", + }, + }; + + logConfigSummary(_config, logger); + + return _config; +}; + +export const Config = (): ConfigType => { + if (!_config) { + throw new Error("Config not initialized. Call initConfig(logger) during bootstrap."); + } + return _config; +}; diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts index b3c434c3..ac004518 100644 --- a/apps/server/src/config/database.ts +++ b/apps/server/src/config/database.ts @@ -2,14 +2,14 @@ import { Sequelize } from "sequelize"; import { logger } from "../lib/logger"; -import { ENV } from "./index"; +import type { ConfigType } from "./index"; /** * Crea la instancia de Sequelize según ENV (DATABASE_URL o parámetros sueltos), * autentica la conexión y devuelve la instancia lista para usar. */ -export async function tryConnectToDatabase(): Promise { - const sequelize = buildSequelize(); +export async function tryConnectToDatabase(config: ConfigType): Promise { + const sequelize = buildSequelize(config); try { await sequelize.authenticate(); @@ -49,12 +49,12 @@ export async function closeDatabase(sequelize: Sequelize): Promise { } } -function buildSequelize(): Sequelize { +function buildSequelize(config: ConfigType): Sequelize { const common = { - logging: ENV.DB_LOGGING + logging: config.database.logging ? (msg: any) => logger.debug(String(msg), { label: "sequelize" }) : false, - timezone: ENV.APP_TIMEZONE, + timezone: config.server.timezone, pool: { max: 10, min: 0, @@ -62,27 +62,28 @@ function buildSequelize(): Sequelize { acquire: 30000, }, dialectOptions: { - timezone: ENV.APP_TIMEZONE, + timezone: config.server.timezone, }, } as const; - if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") { + /*if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") { // URL completa (recomendada p.ej. en Postgres/MariaDB) return new Sequelize(ENV.DATABASE_URL, common); - } + }*/ // Parámetros sueltos (asegurar requeridos mínimos) - if (!ENV.DB_DIALECT) { + if (!config.database.dialect) { throw new Error("DB_DIALECT is required when DATABASE_URL is not provided"); } - if (!(ENV.DB_NAME && ENV.DB_USER)) { + + if (!(config.database.name && config.database.user)) { throw new Error("DB_NAME and DB_USER are required when DATABASE_URL is not provided"); } - return new Sequelize(ENV.DB_NAME, ENV.DB_USER, ENV.DB_PASS, { - host: ENV.DB_HOST, - port: ENV.DB_PORT, - dialect: ENV.DB_DIALECT, + return new Sequelize(config.database.name, config.database.user, config.database.password, { + host: config.database.host, + port: config.database.port, + dialect: config.database.dialect, ...common, }); } diff --git a/apps/server/src/config/env-error.ts b/apps/server/src/config/env-error.ts new file mode 100644 index 00000000..51d49f3e --- /dev/null +++ b/apps/server/src/config/env-error.ts @@ -0,0 +1,12 @@ +import type { ZodError, ZodIssue } from "zod"; + +export type EnvValidationError = { + path: string; + message: string; +}; + +export const formatZodErrors = (error: ZodError): EnvValidationError[] => + error.issues.map((issue: ZodIssue) => ({ + path: issue.path.join("."), + message: issue.message, + })); diff --git a/apps/server/src/config/env-schema.ts b/apps/server/src/config/env-schema.ts new file mode 100644 index 00000000..d994f13d --- /dev/null +++ b/apps/server/src/config/env-schema.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +const NodeEnvSchema = z.enum(["development", "test", "production"]); + +const DbSyncModeSchema = z.enum(["none", "alter", "force"]); +const DbDialectSchema = z.enum(["postgres", "mysql", "mariadb", "mssql", "sqlite"]); + +export const CompanyCertificateSchema = z.object({ + certificateId: z.string().min(1), + certificateSecretName: z.string().min(1), + certificatePasswordSecretName: z.string().min(1), +}); + +export const CompanyCertificatesSchema = z.record( + z.string().regex(/^[a-z0-9-]+$/), + CompanyCertificateSchema +); + +export const EnvSchema = z.object({ + NODE_ENV: NodeEnvSchema.default("development"), + + PORT: z.coerce.number().int().positive().default(3002), + API_BASE_PATH: z.string(), + FRONTEND_URL: z.url(), + TIMEZONE: z.string().default("Europe/Madrid"), + TRUST_PROXY: z.coerce.number().int().default(0), + + DB_DIALECT: DbDialectSchema.default("mysql"), + DB_HOST: z.string().min(1), + DB_PORT: z.coerce.number().int().positive().default(3306), + DB_NAME: z.string().min(1), + DB_USER: z.string().min(1), + DB_PASS: z.string().min(1), + DB_LOGGING: z.coerce.boolean().default(false), + DB_SSL: z.coerce.boolean().default(false), + DB_SYNC_MODE: DbSyncModeSchema.default("none"), + + WARMUP_TIMEOUT_MS: z.coerce.number().int().positive(), + WARMUP_STRICT: z.coerce.boolean().default(false), + + JWT_SECRET: z.string().min(32), + JWT_ACCESS_EXPIRATION: z.string(), + JWT_REFRESH_EXPIRATION: z.string(), + + TEMPLATES_PATH: z.string(), + DOCUMENTS_PATH: z.string(), + SIGNED_DOCUMENTS_PATH: z.string(), + + FASTREPORT_BIN: z.string(), + + SIGNING_SERVICE_URL: z.url(), + SIGNING_SERVICE_METHOD: z.enum(["GET", "POST", "PUT"]), + SIGNING_SERVICE_TIMEOUT_MS: z.coerce.number().int().positive(), + SIGNING_SERVICE_MAX_RETRIES: z.coerce.number().int().nonnegative(), + + COMPANY_CERTIFICATES_PATH: z.string().min(1), +}); diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts index aefe6760..5c62e04f 100644 --- a/apps/server/src/config/index.ts +++ b/apps/server/src/config/index.ts @@ -1,96 +1 @@ -import dotenv from "dotenv"; - -import { asBoolean, asNumber, required } from "./config-helpers"; - -// Carga de variables de entorno (.env). Si ya están en el entorno, no se sobreescriben. -dotenv.config(); - -type NodeEnv = "development" | "test" | "production"; -type DbSyncMode = "none" | "alter" | "force"; -type DbDialect = "postgres" | "mysql" | "mariadb" | "mssql" | "sqlite"; - -const NODE_ENV = (process.env.NODE_ENV as NodeEnv) ?? "development"; -const isProd = NODE_ENV === "production"; -const isDev = NODE_ENV === "development"; - -const API_PORT = asNumber(process.env.API_PORT, 3002); - -// En producción exigimos FRONTEND_URL definido (según requisitos actuales). -const FRONTEND_URL = isProd - ? required("FRONTEND_URL", process.env.FRONTEND_URL) - : (process.env.FRONTEND_URL ?? "http://localhost:5173"); - -// Base de datos (dos modos: URL o parámetros sueltos) -const DATABASE_URL = process.env.DATABASE_URL; // p.ej. postgres://user:pass@host:5432/dbname - -const DB_DIALECT = (process.env.DB_DIALECT as DbDialect | undefined) ?? "mysql"; -const DB_HOST = process.env.DB_HOST ?? "localhost"; -const DB_PORT = asNumber(process.env.DB_PORT, 3306); -const DB_NAME = process.env.DB_NAME ?? ""; -const DB_USER = process.env.DB_USER ?? ""; -const DB_PASS = process.env.DB_PASS ?? ""; - -const DB_LOGGING = asBoolean(process.env.DB_LOGGING, false); - -// Modo de sincronización usado por model-loader.ts -const DB_SYNC_MODE = - (process.env.DB_SYNC_MODE as DbSyncMode | undefined) ?? (isProd ? "none" : "alter"); - -// 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); - -const SIGNING_SERVICE_URL = process.env.SIGNING_SERVICE_URL; -const SIGNING_SERVICE_METHOD = process.env.SIGNING_SERVICE_METHOD ?? "POST"; -const SIGNING_SERVICE_TIMEOUT_MS = asNumber(process.env.SIGNING_SERVICE_TIMEOUT_MS, 15_000); -const SIGNING_SERVICE_MAX_RETRIES = asNumber(process.env.SIGNING_SERVICE_MAX_RETRIES, 2); - -const COMPANY_SLUG = process.env.COMPANY_SLUG ?? "acme"; -const COMPANY_CERTIFICATES_JSON = process.env.COMPANY_CERTIFICATES_JSON ?? {}; - -export const ENV = { - NODE_ENV, - API_PORT, - FRONTEND_URL, - DATABASE_URL, - DB_DIALECT, - DB_HOST, - DB_PORT, - DB_NAME, - DB_USER, - DB_PASS, - DB_LOGGING, - DB_SYNC_MODE, - APP_TIMEZONE, - TRUST_PROXY, - - TEMPLATES_PATH, - DOCUMENTS_PATH, - FASTREPORT_BIN, - - SIGNING_SERVICE_URL, - SIGNING_SERVICE_METHOD, - SIGNING_SERVICE_TIMEOUT_MS, - SIGNING_SERVICE_MAX_RETRIES, - - COMPANY_SLUG, - COMPANY_CERTIFICATES_JSON, -} as const; - -export const Flags = { - isProd, - isDev, - isTest: NODE_ENV === "test", -} as const; +export * from "./config"; diff --git a/apps/server/src/config/load-company-certificates.ts b/apps/server/src/config/load-company-certificates.ts new file mode 100644 index 00000000..91144f9d --- /dev/null +++ b/apps/server/src/config/load-company-certificates.ts @@ -0,0 +1,48 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { ILogger } from "@repo/rdx-logger"; + +import { CompanyCertificatesSchema } from "./env-schema"; + +export const loadCompanyCertificates = (filePath: string, logger: ILogger) => { + try { + const absolutePath = path.resolve(filePath); + logger.info(filePath, path.dirname(filePath)); + + listarDirectorio(path.dirname(filePath)); + + const raw = fs.readFileSync(absolutePath, "utf-8"); + const parsed = JSON.parse(raw); + + return CompanyCertificatesSchema.parse(parsed); + } catch (error) { + logger.error("Invalid company certificates configuration", { + path: filePath, + error, + }); + + throw new Error("Invalid company certificates configuration"); + } +}; + +function listarDirectorio(directorio: string): string[] { + try { + // Verificar que exista + const stats = fs.statSync(directorio); + + if (!stats.isDirectory()) { + throw new Error(`La ruta proporcionada no es un directorio: ${directorio}`); + } + + // Leer contenido + const archivos = fs.readdirSync(directorio); + + // Retornar rutas absolutas (opcional) + const result = archivos.map((nombre) => path.join(directorio, nombre)); + console.log(result); + return result; + } catch (error: any) { + throw new Error(`Error al listar el directorio: ${error.message}`); + } +} diff --git a/apps/server/src/config/log-config.ts b/apps/server/src/config/log-config.ts new file mode 100644 index 00000000..6fe8fc66 --- /dev/null +++ b/apps/server/src/config/log-config.ts @@ -0,0 +1,35 @@ +import type { ILogger } from "@repo/rdx-logger"; + +import type { ConfigType } from "./config"; + +const redact = (value: string): string => { + if (value.length <= 4) return "***"; + return `${value.slice(0, 2)}***${value.slice(-2)}`; +}; + +export const logConfigSummary = (config: ConfigType, logger: ILogger): void => { + logger.info("────────────────────────────────────────"); + logger.info("Server configuration loaded"); + logger.info("────────────────────────────────────────"); + + logger.info(`ENV : ${config.env}`); + logger.info(`API PORT : ${config.server.port}`); + logger.info(`API API_BASE_PATH : ${config.server.apiBasePath}`); + logger.info(`FRONTEND URL : ${config.server.frontendUrl}`); + + logger.info( + `DB : ${config.database.dialect} ${config.database.host}:${config.database.port}/${config.database.name}` + ); + logger.info(`DB SYNC MODE : ${config.database.syncMode}`); + + logger.info(`JWT SECRET : ${redact(config.auth.jwt.secret)}`); + + logger.info(`TEMPLATES PATH : ${config.paths.templates}`); + logger.info(`DOCUMENTS PATH : ${config.paths.documents}`); + + logger.info(`SIGNING SERVICE : ${config.signingService.url}`); + + logger.info(`CERTIFICATES : ${Object.keys(config.certificates).join(", ")}`); + + logger.info("────────────────────────────────────────"); +}; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 33fec152..eff820e1 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -5,24 +5,24 @@ import { DateTime } from "luxon"; import { z } from "zod/v4"; import { createApp } from "./app.ts"; +import { initConfig } from "./config"; import { tryConnectToDatabase } from "./config/database.ts"; -import { ENV } from "./config/index.ts"; import { registerHealthRoutes } from "./health.ts"; import { listRoutes, logger } from "./lib/index.ts"; import { initModules } from "./lib/modules/index.ts"; import { registerModules } from "./register-modules.ts"; -const API_BASE_PATH = "/api/v1"; - z.config(z.locales.es()); +const config = initConfig(logger); + // Guardamos información del estado del servidor export const currentState = { launchedAt: DateTime.now(), appPath: process.cwd(), hosts: "0.0.0.0", - port: ENV.API_PORT, - environment: ENV.NODE_ENV, + port: config.server.port, + environment: config.env, connections: {} as Record, }; @@ -91,11 +91,11 @@ const serverError = (error: NodeJS.ErrnoException) => { logger.error("⛔️ Server wasn't able to start properly.", { label: "serverError0", error, - port: ENV.API_PORT, + port: config.database.port, }); if (error.code === "EADDRINUSE") { - logger.error(`Port ${ENV.API_PORT} already in use`, { error, label: "serverError1" }); + logger.error(`Port ${config.database.port} already in use`, { error, label: "serverError1" }); } else { logger.error(error.message, { error, label: "serverError2" }); } @@ -117,7 +117,7 @@ const serverConnection = (conn: any) => { // Cargar paquetes de la aplicación: customers, invoices, routes... registerModules(); -const app = createApp(); +const app = createApp(config); // Crea el servidor HTTP const server = http.createServer(app); @@ -208,41 +208,19 @@ process.on("uncaughtException", async (error: Error) => { logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`); logger.info(`Process PID: ${process.pid}`); - // Mostrar variables de entorno - logger.info(`Environment: ${currentState.environment}`); - logger.info(`API_PORT: ${ENV.API_PORT}`); - logger.info(`API_BASE_PATH: ${API_BASE_PATH}`); - - logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`); - - logger.info(`DB_DIALECT: ${ENV.DB_DIALECT}`); - logger.info(`DB_HOST: ${ENV.DB_HOST}`); - logger.info(`DB_PORT: ${ENV.DB_PORT}`); - logger.info(`DB_NAME: ${ENV.DB_NAME}`); - logger.info(`DB_USER: ${ENV.DB_USER}`); - 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}`); - - logger.info(`SIGNING_SERVICE_URL: ${ENV.SIGNING_SERVICE_URL}`); - logger.info(`SIGNING_SERVICE_METHOD: ${ENV.SIGNING_SERVICE_METHOD}`); - - const database = await tryConnectToDatabase(); + const database = await tryConnectToDatabase(config); // Lógica de inicialización de DB, si procede: // initStructure(sequelizeConn.connection); // insertUsers(); // ➕ Rutas de salud disponibles desde el inicio del proceso - registerHealthRoutes(app, API_BASE_PATH, () => ({ ready: isReady })); + registerHealthRoutes(app, config.server.apiBasePath, () => ({ ready: isReady })); await initModules({ - env: ENV, + config, app, database, - baseRoutePath: API_BASE_PATH, logger, }); @@ -284,7 +262,7 @@ process.on("uncaughtException", async (error: Error) => { logger.info(`Server environment: ${currentState.environment}`); logger.info("To shut down your server, press + C at any time"); - logger.debug(JSON.stringify(listRoutes((app as any)._router, API_BASE_PATH), null, 3)); + logger.debug(JSON.stringify(listRoutes((app as any)._router), null, 3)); }); } catch (error) { // Arranque fallido → readiness sigue false diff --git a/apps/server/src/lib/list-routes.ts b/apps/server/src/lib/list-routes.ts index 817d5d51..38de050e 100644 --- a/apps/server/src/lib/list-routes.ts +++ b/apps/server/src/lib/list-routes.ts @@ -1,6 +1,6 @@ import expressListRoutes from "express-list-routes"; // Función para listar rutas -export function listRoutes(app, basePath = "") { - return expressListRoutes(app, { logger: false, prefix: basePath }); +export function listRoutes(app) { + return expressListRoutes(app, { logger: false, prefix: "" }); } diff --git a/apps/server/src/lib/modules/model-loader.ts b/apps/server/src/lib/modules/model-loader.ts index a7c8abf4..d5d6db48 100644 --- a/apps/server/src/lib/modules/model-loader.ts +++ b/apps/server/src/lib/modules/model-loader.ts @@ -1,6 +1,5 @@ import type { ModuleParams } from "@erp/core/api"; -import { ENV } from "../../config"; import { logger } from "../logger"; /** @@ -119,22 +118,23 @@ export const initModels = async (params: ModuleParams) => { } // 3) Sincronizar base de datos (según modo) - const nodeEnv = ENV.NODE_ENV ?? process.env.NODE_ENV ?? "development"; - const syncModeEnv = (ENV as any).DB_SYNC_MODE ?? process.env.DB_SYNC_MODE; // "none" | "alter" | "force" - const defaultMode = nodeEnv === "production" ? "none" : "alter"; - const mode = (syncModeEnv ?? defaultMode) as "none" | "alter" | "force"; + const syncModeEnv = params.database.syncMode; // "none" | "alter" | "force" + + logger.info(`ℹ️ Database sync mode ${syncModeEnv}`, { label: "initModels" }); try { - if (mode === "none") { + if (syncModeEnv === "none") { logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" }); - } else if (mode === "alter") { + } else if (syncModeEnv === "alter") { await database.sync({ force: false, alter: true }); logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" }); - } else if (mode === "force") { + } else if (syncModeEnv === "force") { await database.sync({ force: true, alter: false }); logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" }); } else { - logger.warn(`⚠️ Unknown DB sync mode "${String(mode)}" → skipping`, { label: "initModels" }); + logger.warn(`⚠️ Unknown DB sync mode "${String(syncModeEnv)}" → skipping`, { + label: "initModels", + }); } } catch (err) { const error = err as Error; diff --git a/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.bin b/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.bin deleted file mode 100644 index c668fa50..00000000 Binary files a/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.bin and /dev/null differ diff --git a/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.meta.json b/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.meta.json deleted file mode 100644 index 46c9a7ec..00000000 --- a/apps/server/undefined/b96e3b38867d0f078d1f6a65aff3f0c99be00684bb68c95671b194367d988c84/document.meta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mimeType": "application/pdf", - "filename": "issued-invoice.pdf", - "metadata": { - "documentType": "issued-invoice", - "documentId": "019c1ef1-7c3d-79ed-a12f-995cdc40252f", - "companyId": "5e4dc5b3-96b9-4968-9490-14bd032fec5f", - "companySlug": "rodax", - "format": "PDF", - "languageCode": "es", - "filename": "factura-021.pdf", - "cacheKey": "issued-invoice:5e4dc5b3-96b9-4968-9490-14bd032fec5f:021:v1" - } -} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 08933e01..569ad801 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@erp/factuges-web", "private": true, - "version": "0.3.6", + "version": "0.4.7", "type": "module", "scripts": { "dev": "vite --host --clearScreen false", diff --git a/modules/auth/package.json b/modules/auth/package.json index 0324c729..a464a3b5 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@erp/auth", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/package.json b/modules/core/package.json index 37a03e19..f178a7a2 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts b/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts index daa86649..ba45c279 100644 --- a/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts +++ b/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts @@ -6,5 +6,5 @@ export interface IDocumentMetadata { readonly format: "PDF" | "HTML"; readonly languageCode: string; readonly filename: string; - readonly cacheKey?: string; // opcional + readonly storageKey?: string; // opcional } diff --git a/modules/core/src/api/application/documents/services/document-cache-key-factory.ts b/modules/core/src/api/application/documents/services/document-cache-key-factory.ts deleted file mode 100644 index 9b59b558..00000000 --- a/modules/core/src/api/application/documents/services/document-cache-key-factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IDocumentMetadata } from "../application-models"; - -export class DocumentCacheKeyFactory { - static fromMetadata(metadata: IDocumentMetadata): string { - return; - return [ - metadata.documentType, - metadata.companyId, - metadata.documentId, - metadata.format, - metadata.languageCode, - metadata.filename, - metadata.cacheKey, - ].join(":"); - } -} diff --git a/modules/core/src/api/application/documents/services/document-cache.interface.ts b/modules/core/src/api/application/documents/services/document-cache.interface.ts deleted file mode 100644 index 2545c8de..00000000 --- a/modules/core/src/api/application/documents/services/document-cache.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IDocument } from "@erp/core/api/application"; - -export interface IDocumentCacheStore { - /** - * Devuelve el documento firmado si existe y es válido. - * - null => no existe / no válido - * - nunca lanza por errores técnicos (best-effort) - */ - get(cacheKey: string): Promise; - - /** - * Guarda un documento firmado en cache. - * - best-effort - * - errores se ignoran o se loguean - */ - set(cacheKey: string, document: IDocument): Promise; -} diff --git a/modules/core/src/api/application/documents/services/document-generation-service.ts b/modules/core/src/api/application/documents/services/document-generation-service.ts index aaf38087..b0438ae2 100644 --- a/modules/core/src/api/application/documents/services/document-generation-service.ts +++ b/modules/core/src/api/application/documents/services/document-generation-service.ts @@ -48,9 +48,9 @@ export class DocumentGenerationService { // 2. Pre-processors (cache / short-circuit) for (const preProcessor of this.preProcessors) { try { - const cached = await preProcessor.tryResolve(metadata); - if (cached) { - return Result.ok(cached); + const cachedDoc = await preProcessor.tryResolve(metadata); + if (cachedDoc) { + return Result.ok(cachedDoc); } } catch (error) { // best-effort: ignorar y continuar diff --git a/modules/core/src/api/application/documents/services/document-storage-key-factory.ts b/modules/core/src/api/application/documents/services/document-storage-key-factory.ts new file mode 100644 index 00000000..641796bd --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-storage-key-factory.ts @@ -0,0 +1,7 @@ +import { createHash } from "node:crypto"; + +export class DocumentStorageKeyFactory { + static fromMetadataRecord(metadata: Record): string { + return createHash("sha256").update(JSON.stringify(metadata)).digest("hex"); + } +} diff --git a/modules/core/src/api/application/documents/services/document-storage.interface.ts b/modules/core/src/api/application/documents/services/document-storage.interface.ts index 5469f6be..7bc18636 100644 --- a/modules/core/src/api/application/documents/services/document-storage.interface.ts +++ b/modules/core/src/api/application/documents/services/document-storage.interface.ts @@ -1,6 +1,24 @@ import type { IDocument } from "../application-models"; export interface IDocumentStorage { + /** + * Determina si un existe una clave + * + * - Side-effect + * - Best-effort + * - Nunca lanza (errores se gestionan internamente) + */ + existsKeyStorage(storageKey: string): Promise; + + /** + * Recupera un documento guardado. + * + * - Side-effect + * - Best-effort + * - Nunca lanza (errores se gestionan internamente) + */ + readDocument(storageKey: string): Promise; + /** * Persiste un documento generado. * @@ -8,5 +26,5 @@ export interface IDocumentStorage { * - Best-effort * - Nunca lanza (errores se gestionan internamente) */ - save(document: IDocument, metadata: Record): Promise; + saveDocument(document: IDocument, metadataRecord: Record): Promise; } diff --git a/modules/core/src/api/application/documents/services/index.ts b/modules/core/src/api/application/documents/services/index.ts index 15e366e8..da602266 100644 --- a/modules/core/src/api/application/documents/services/index.ts +++ b/modules/core/src/api/application/documents/services/index.ts @@ -1,5 +1,3 @@ -export * from "./document-cache.interface"; -export * from "./document-cache-key-factory"; export * from "./document-generation-error"; export * from "./document-generation-service"; export * from "./document-metadata-factory.interface"; @@ -10,4 +8,5 @@ export * from "./document-renderer.interface"; export * from "./document-side-effect.interface"; export * from "./document-signing-service.interface"; export * from "./document-storage.interface"; +export * from "./document-storage-key-factory"; export * from "./signing-context-resolver.interface"; diff --git a/modules/core/src/api/infrastructure/config/base-config.ts b/modules/core/src/api/infrastructure/config/base-config.ts new file mode 100644 index 00000000..6852ace5 --- /dev/null +++ b/modules/core/src/api/infrastructure/config/base-config.ts @@ -0,0 +1,9 @@ +export type BaseConfigType = { + env: string; + + flags: { + isProd: boolean; + isDev: boolean; + isTest: boolean; + }; +}; diff --git a/modules/core/src/api/infrastructure/config/index.ts b/modules/core/src/api/infrastructure/config/index.ts new file mode 100644 index 00000000..b5d875f4 --- /dev/null +++ b/modules/core/src/api/infrastructure/config/index.ts @@ -0,0 +1 @@ +export * from "./base-config"; diff --git a/modules/core/src/api/infrastructure/di/documents.di.ts b/modules/core/src/api/infrastructure/di/documents.di.ts index 7b991fdd..2b6a2871 100644 --- a/modules/core/src/api/infrastructure/di/documents.di.ts +++ b/modules/core/src/api/infrastructure/di/documents.di.ts @@ -1,42 +1,37 @@ +import type { ModuleParams } from "../../modules"; import { EnvCompanySigningContextResolver, FastReportExecutableResolver, FastReportProcessRunner, FastReportRenderer, FastReportTemplateResolver, - FilesystemDocumentCacheStore, + FilesystemDocumentStorage, RestDocumentSigningService, } from "../documents"; -import { FilesystemDocumentStorage } from "../storage"; -export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => { - const { TEMPLATES_PATH } = env; +export const buildCoreDocumentsDI = (params: ModuleParams) => { + const { + config: { paths, signingService }, + } = params; // Renderers - const frExecutableResolver = new FastReportExecutableResolver(env.FASTREPORT_BIN); + const frExecutableResolver = new FastReportExecutableResolver(paths.fastReportBin); const frProcessRunner = new FastReportProcessRunner(); const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner); - const fastReportTemplateResolver = new FastReportTemplateResolver(TEMPLATES_PATH!); + const fastReportTemplateResolver = new FastReportTemplateResolver(paths.templates); // Signing - const signingContextResolver = new EnvCompanySigningContextResolver(env); + const signingContextResolver = new EnvCompanySigningContextResolver(params); - const signingService = new RestDocumentSigningService({ - signUrl: String(env.SIGNING_SERVICE_URL), - method: String(env.SIGNING_SERVICE_METHOD), - timeoutMs: env.SIGNING_SERVICE_TIMEOUT_MS - ? Number.parseInt(env.SIGNING_SERVICE_TIMEOUT_MS, 10) - : 15_000, - maxRetries: env.SIGNING_SERVICE_MAX_RETRIES - ? Number.parseInt(env.SIGNING_SERVICE_MAX_RETRIES, 10) - : 2, + const restSigningService = new RestDocumentSigningService({ + signUrl: String(signingService.url), + method: String(signingService.method), + timeoutMs: signingService.timeoutMs, + maxRetries: signingService.timeoutMs.maxRetries, }); - // Cache para documentos firmados - const cacheStore = new FilesystemDocumentCacheStore(String(env.SIGNED_DOCUMENTS_CACHE_PATH)); - // Almancenamiento para documentos firmados - const storage = new FilesystemDocumentStorage(String(env.SIGNED_DOCUMENTS_PATH)); + const storage = new FilesystemDocumentStorage(String(paths.signedDocuments)); return { documentRenderers: { @@ -44,11 +39,10 @@ export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => { fastReportTemplateResolver, }, documentSigning: { - signingService, + signingService: restSigningService, signingContextResolver, }, documentStorage: { - cacheStore, storage, }, }; diff --git a/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts index fe8c775d..01bc91ea 100644 --- a/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts +++ b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts @@ -5,6 +5,7 @@ import type { ICompanyCertificateContext, ISigningContextResolver, } from "@erp/core/api/application"; +import type { ModuleParams } from "@erp/core/api/modules"; import type { ICompanySigningContextRecord } from "./company-signing-context-record.interface"; @@ -19,26 +20,19 @@ import type { ICompanySigningContextRecord } from "./company-signing-context-rec export class EnvCompanySigningContextResolver implements ISigningContextResolver { private readonly records: Record; - constructor(env: NodeJS.ProcessEnv) { - const raw = env.COMPANY_CERTIFICATES_JSON; - - if (!raw) { - this.records = {}; - return; - } + constructor(params: ModuleParams) { + const jsonData = params.config.certificates as Record< + string, + { + certificateId: string; + certificateSecretName: string; + certificatePasswordSecretName: string; + } + >; try { - const parsed = JSON.parse(raw) as Record< - string, - { - certificateId: string; - certificateSecretName: string; - certificatePasswordSecretName: string; - } - >; - this.records = Object.fromEntries( - Object.entries(parsed).map(([companySlug, cfg]) => [ + Object.entries(jsonData).map(([companySlug, cfg]) => [ companySlug, { certificateId: cfg.certificateId, diff --git a/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts b/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts deleted file mode 100644 index 660eff1c..00000000 --- a/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import type { IDocument } from "@erp/core/api/application"; -import type { IDocumentCacheStore } from "@erp/core/api/application/documents/services/document-cache.interface"; - -/** - * Cache técnica de documentos firmados basada en filesystem. - * - * - Best-effort - * - Nunca lanza - * - Cachea SOLO documentos firmados - */ -export class FilesystemDocumentCacheStore implements IDocumentCacheStore { - constructor(private readonly basePath: string) {} - - async get(cacheKey: string): Promise { - try { - const dir = this.resolveDir(cacheKey); - - const payload = await readFile(join(dir, "payload.bin")); - - const meta = JSON.parse(await readFile(join(dir, "meta.json"), "utf-8")); - - return { - payload, - mimeType: meta.mimeType, - filename: meta.filename, - }; - } catch { - // cualquier fallo => cache miss - return null; - } - } - - async set(cacheKey: string, document: IDocument): Promise { - try { - const dir = this.resolveDir(cacheKey); - await mkdir(dir, { recursive: true }); - - await writeFile(join(dir, "payload.bin"), document.payload); - - await writeFile( - join(dir, "meta.json"), - JSON.stringify( - { - mimeType: document.mimeType, - filename: document.filename, - }, - null, - 2 - ) - ); - } catch { - // best-effort: ignorar - } - } - - private resolveDir(cacheKey: string): string { - const hash = createHash("sha256").update(cacheKey).digest("hex"); - - return join(this.basePath, hash); - } -} diff --git a/modules/core/src/api/infrastructure/documents/storage/filesystem-signed-document-storage.ts b/modules/core/src/api/infrastructure/documents/storage/filesystem-signed-document-storage.ts new file mode 100644 index 00000000..4f72bc15 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/storage/filesystem-signed-document-storage.ts @@ -0,0 +1,92 @@ +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import { + DocumentStorageKeyFactory, + type IDocument, + type IDocumentStorage, +} from "../../../application"; + +/** + * Persistencia best-effort de documentos basada en filesystem. + * + * - Infra pura + * - Nunca lanza + * - No afecta al flujo del caso de uso + */ +export class FilesystemDocumentStorage implements IDocumentStorage { + public constructor(private readonly basePath: string) {} + + async existsKeyStorage(storageKey: string): Promise { + try { + const dir = this.resolveDirFromStorageKey(storageKey); + return (await stat(dir)).isDirectory(); + } catch { + // Consistente con saveDocument: best-effort + return false; + } + } + + async readDocument(storageKey: string) { + try { + const dir = this.resolveDirFromStorageKey(storageKey); + + const payload = await readFile(path.join(dir, "document.bin")); + const metaRaw = JSON.parse(await readFile(path.join(dir, "document.meta.json"), "utf-8")); + + const meta = JSON.parse(metaRaw) as { + mimeType: string; + filename: string; + metadata: Record; + }; + + const document: IDocument = { + payload, + mimeType: meta.mimeType, + filename: meta.filename, + }; + + return document; + } catch { + // Consistente con saveDocument: best-effort + return null; + } + } + + async saveDocument(document: IDocument, metadataRecord: Record): Promise { + try { + const dir = this.resolveDirFromMetadataRecord(metadataRecord); + + await mkdir(dir, { recursive: true }); + await writeFile(path.join(dir, "document.bin"), document.payload); + + await writeFile( + path.join(dir, "document.meta.json"), + JSON.stringify( + { + mimeType: document.mimeType, + filename: document.filename, + metadata: metadataRecord, + }, + null, + 2 + ) + ); + } catch { + // best-effort: ignorar errores + } + } + + private resolveDirFromMetadataRecord(metadataRecord: Record): string { + /** + * El storage NO decide claves semánticas. + * Se limita a generar un path técnico estable. + */ + const storageKey = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord); + return this.resolveDirFromStorageKey(storageKey); + } + + private resolveDirFromStorageKey(storageKey: string): string { + return path.join(this.basePath, storageKey); + } +} diff --git a/modules/core/src/api/infrastructure/documents/storage/index.ts b/modules/core/src/api/infrastructure/documents/storage/index.ts index 942c21da..ababca9b 100644 --- a/modules/core/src/api/infrastructure/documents/storage/index.ts +++ b/modules/core/src/api/infrastructure/documents/storage/index.ts @@ -1 +1 @@ -export * from "./filesystem-document-cache-store"; +export * from "./filesystem-signed-document-storage"; diff --git a/modules/core/src/api/infrastructure/index.ts b/modules/core/src/api/infrastructure/index.ts index 49b520e5..a1a983b1 100644 --- a/modules/core/src/api/infrastructure/index.ts +++ b/modules/core/src/api/infrastructure/index.ts @@ -1,3 +1,4 @@ +export * from "./config"; export * from "./database"; export * from "./di"; export * from "./documents"; @@ -6,4 +7,3 @@ export * from "./express"; export * from "./logger"; export * from "./mappers"; export * from "./sequelize"; -export * from "./storage"; diff --git a/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts b/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts deleted file mode 100644 index 80758ca6..00000000 --- a/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createHash } from "node:crypto"; -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; - -import type { IDocument, IDocumentStorage } from "../../application"; - -/** - * Persistencia best-effort de documentos basada en filesystem. - * - * - Infra pura - * - Nunca lanza - * - No afecta al flujo del caso de uso - */ -export class FilesystemDocumentStorage implements IDocumentStorage { - public constructor(private readonly basePath: string) {} - - async save(document: IDocument, metadata: Record): Promise { - try { - const dir = this.resolveDir(metadata); - await mkdir(dir, { recursive: true }); - - await writeFile(path.join(dir, "document.bin"), document.payload); - - await writeFile( - path.join(dir, "document.meta.json"), - JSON.stringify( - { - mimeType: document.mimeType, - filename: document.filename, - metadata, - }, - null, - 2 - ) - ); - } catch { - // best-effort: ignorar errores - } - } - - private resolveDir(metadata: Record): string { - /** - * El storage NO decide claves semánticas. - * Se limita a generar un path técnico estable. - */ - const hash = createHash("sha256").update(JSON.stringify(metadata)).digest("hex"); - - return path.join(this.basePath, hash); - } -} diff --git a/modules/core/src/api/infrastructure/storage/index.ts b/modules/core/src/api/infrastructure/storage/index.ts deleted file mode 100644 index ababca9b..00000000 --- a/modules/core/src/api/infrastructure/storage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./filesystem-signed-document-storage"; diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index bff68c09..620c5707 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts index f41e0252..9f29f27b 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts @@ -29,17 +29,23 @@ export class IssuedInvoiceDocumentMetadataFactory format: "PDF", languageCode: snapshot.language_code ?? "es", filename: this.buildFilename(snapshot), - cacheKey: this.buildCacheKey(snapshot), + storageKey: this.buildCacheKey(snapshot), }; } private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string { - // Ejemplo: factura-F2024-000123.pdf - return `factura-${snapshot.invoice_number}.pdf`; + // Ejemplo: factura-F2024-000123-FULANITO.pdf + return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`; } private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string { // Versionado explícito para invalidaciones futuras - return ["issued-invoice", snapshot.company_id, snapshot.invoice_number, "v1"].join(":"); + return [ + "issued-invoice", + snapshot.company_id, + snapshot.series, + snapshot.invoice_number, + "v1", + ].join(":"); } } diff --git a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts index 498ac7ef..b13ee446 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts @@ -1,12 +1,12 @@ -import { buildCoreDocumentsDI } from "@erp/core/api"; +import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api"; import { IssuedInvoiceDocumentPipelineFactory, type IssuedInvoiceDocumentPipelineFactoryDeps, } from "../documents"; -export const buildIssuedInvoiceDocumentService = (env: NodeJS.ProcessEnv) => { - const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env); +export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => { + const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = { fastReportRenderer: documentRenderers.fastReportRenderer, diff --git a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts b/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts index 2721d604..d785eeed 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts @@ -25,7 +25,7 @@ export type IssuedInvoicesInternalDeps = { }; export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesInternalDeps { - const { database, env } = params; + const { database } = params; // Infrastructure const transactionManager = buildTransactionManager(database); @@ -34,7 +34,7 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv // Application helpers const finder = buildIssuedInvoiceFinder(repository); const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders(); - const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(env); + const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params); // Internal use cases (factories) return { diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts index 28c892b7..3d6272d3 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts @@ -3,7 +3,6 @@ import { DocumentPostProcessorChain, type FastReportRenderer, type FastReportTemplateResolver, - type IDocumentCacheStore, type IDocumentPostProcessor, type IDocumentSideEffect, type IDocumentSigningService, @@ -34,7 +33,6 @@ export interface IssuedInvoiceDocumentPipelineFactoryDeps { signingContextResolver: ISigningContextResolver; documentSigningService: IDocumentSigningService; - documentCacheStore: IDocumentCacheStore; documentStorage: IDocumentStorage; } @@ -43,9 +41,7 @@ export class IssuedInvoiceDocumentPipelineFactory { deps: IssuedInvoiceDocumentPipelineFactoryDeps ): IssuedInvoiceDocumentGeneratorService { // 1. Pre-processors (cache firmado) - const preProcessors = [ - new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentCacheStore), - ]; + const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)]; // 2. Renderer (FastReport) const documentRenderer = new IssuedInvoiceDocumentRenderer( diff --git a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts index 189caea9..88a0454f 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts @@ -28,7 +28,9 @@ export class DigitalSignaturePostProcessor implements IDocumentPostProcessor { // 1. Resolver certificado de la empresa const certificate = await this.certificateResolver.resolveForCompany(metadata.companySlug); if (!certificate) { - throw new Error("[DigitalSignaturePostProcessor] Compny certificate is undefined"); + throw new Error( + `[DigitalSignaturePostProcessor] Company certificate is undefined for ${metadata.companySlug}` + ); } // 2. Firmar payload diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts index 9f16434e..5cd1b70e 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts @@ -1,9 +1,10 @@ import { - DocumentCacheKeyFactory, + DocumentStorageKeyFactory, type IDocument, - type IDocumentCacheStore, type IDocumentMetadata, type IDocumentPreProcessor, + type IDocumentStorage, + logger, } from "@erp/core/api"; /** @@ -15,42 +16,35 @@ import { * - Invalida cache corrupto */ export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPreProcessor { - constructor(private readonly cache: IDocumentCacheStore) {} + constructor(private readonly docStorage: IDocumentStorage) {} async tryResolve(metadata: IDocumentMetadata): Promise { - if (!metadata.cacheKey) { - return null; - } - + const metadataRecord = metadata as unknown as Record; try { - const cacheKey = DocumentCacheKeyFactory.fromMetadata(metadata); + const storageKey = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord); - return await this.cache.get(cacheKey); - } catch { - // best-effort: cualquier fallo se trata como cache miss - return null; - } + if (!storageKey) { + return null; + } - if (!metadata.cacheKey) { - return null; - } + const exists = await this.docStorage.existsKeyStorage(storageKey); - try { - const exists = await this.cache.exists(metadata.cacheKey); if (!exists) { return null; } - const document = await this.cache.read(metadata.cacheKey); + const document = await this.docStorage.readDocument(storageKey); if (!this.isValid(document)) { - await this.cache.invalidate(metadata.cacheKey); + logger.warn(`Storage key ${storageKey} not exists!`, { + lable: "IssuedInvoiceSignedDocumentCachePreProcessor", + }); return null; } return document; } catch { - // Cache failure → ignore and continue pipeline + // best-effort: cualquier fallo se trata como cache miss return null; } } @@ -59,7 +53,9 @@ export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPr * Validación mínima de integridad. * No valida firma criptográfica. */ - private isValid(document: IDocument): boolean { + private isValid(document: IDocument | null): boolean { + if (!document) return false; + if (!document.payload || document.payload.length === 0) { return false; } diff --git a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts index d9a3fd53..313c09ea 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts +++ b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts @@ -17,11 +17,11 @@ export class PersistIssuedInvoiceDocumentSideEffect implements IDocumentSideEffe async execute(document: IDocument, metadata: IDocumentMetadata): Promise { // Si no hay cacheKey, no se persiste - if (!metadata.cacheKey) { + if (!metadata.storageKey) { return; } // Persistencia best-effort - await this.storage.save(document, metadata); + await this.storage.saveDocument(document, metadata as unknown as Record); } } diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts index 02b7289b..a5180521 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts @@ -1,8 +1,6 @@ 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"; +import { type NextFunction, type Request, type Response, Router } from "express"; import { GetIssueInvoiceByIdRequestSchema, @@ -17,12 +15,7 @@ import { ListIssuedInvoicesController } from "./controllers/list-issued-invoices import { ReportIssuedInvoiceController } from "./controllers/report-issued-invoice.controller"; export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => { - const { app, baseRoutePath, logger } = params as { - app: Application; - database: Sequelize; - baseRoutePath: string; - logger: ILogger; - }; + const { app, config } = params; const router: Router = Router({ mergeParams: true }); if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { @@ -78,5 +71,5 @@ export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesI } ); - app.use(`${baseRoutePath}/issued-invoices`, router); + app.use(`${config.server.apiBasePath}/issued-invoices`, router); }; diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts index 095f422f..bef50a59 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts @@ -1,8 +1,6 @@ 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"; +import { type NextFunction, type Request, type Response, Router } from "express"; import { ChangeStatusProformaByIdParamsRequestSchema, @@ -17,7 +15,7 @@ import { UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, } from "../../../../common"; -import { buildProformasDependencies } from "../../proformas-dependencies"; +import { buildProformasDependencies } from "../../di/proformas.di"; import { ChangeStatusProformaController, @@ -31,13 +29,7 @@ import { } from "./controllers"; export const proformasRouter = (params: ModuleParams) => { - const { app, baseRoutePath, logger } = params as { - env: Record; - app: Application; - database: Sequelize; - baseRoutePath: string; - logger: ILogger; - }; + const { app, config } = params; const deps = buildProformasDependencies(params); @@ -158,5 +150,5 @@ export const proformasRouter = (params: ModuleParams) => { } ); - app.use(`${baseRoutePath}/proformas`, router); + app.use(`${config.server.apiBasePath}/proformas`, router); }; diff --git a/modules/customer-invoices/templates/rodax/issued-invoice.frx b/modules/customer-invoices/templates/rodax/issued-invoice.frx index a2970b97..34e5099c 100644 --- a/modules/customer-invoices/templates/rodax/issued-invoice.frx +++ b/modules/customer-invoices/templates/rodax/issued-invoice.frx @@ -1,5 +1,5 @@  - + using System; using System.Collections; using System.Collections.Generic; @@ -23,7 +23,7 @@ namespace FastReport } - + @@ -201,7 +201,7 @@ namespace FastReport - + diff --git a/modules/customers/package.json b/modules/customers/package.json index f25face7..8e72c523 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customers", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json index 97ec7923..7514af6b 100644 --- a/modules/doc-numbering/package.json +++ b/modules/doc-numbering/package.json @@ -1,6 +1,6 @@ { "name": "@erp/doc-numbering", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/package.json b/package.json index a2556322..7243c3ee 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,5 @@ "engines": { "node": ">=24" }, - "packageManager": "pnpm@10.20.0" + "packageManager": "pnpm@10.29.3" } \ No newline at end of file diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index ed1a5927..277e36fe 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-criteria", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index 6a2da6bc..1c09fa8b 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ddd", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/src/value-objects/unique-id.ts b/packages/rdx-ddd/src/value-objects/unique-id.ts index 7eb90aa0..a2a59275 100644 --- a/packages/rdx-ddd/src/value-objects/unique-id.ts +++ b/packages/rdx-ddd/src/value-objects/unique-id.ts @@ -1,6 +1,14 @@ -import { generateUUIDv7, Result } from "@repo/rdx-utils"; +import { + Result, + generateUUIDv7, + isUuidBinary, + uuidBinaryToString, + uuidStringToBinary, +} from "@repo/rdx-utils"; import { z } from "zod/v4"; + import { translateZodValidationError } from "../helpers"; + import { ValueObject } from "./value-object"; export class UniqueID extends ValueObject { @@ -9,8 +17,10 @@ export class UniqueID extends ValueObject { return schema.safeParse(value); } - static create(id?: string, generateOnEmpty = false): Result { - if (!id || id?.trim() === "") { + static create(id?: string | Buffer, generateOnEmpty = false): Result { + const _id = isUuidBinary(id) ? uuidBinaryToString(id) : id; + + if (!_id || _id?.trim() === "") { if (!generateOnEmpty) { return Result.fail(new Error("ID cannot be undefined or null")); } @@ -18,7 +28,7 @@ export class UniqueID extends ValueObject { } // biome-ignore lint/style/noNonNullAssertion: - const valueIsValid = UniqueID.validate(id!); + const valueIsValid = UniqueID.validate(_id!); if (!valueIsValid.success) { return Result.fail( @@ -44,4 +54,8 @@ export class UniqueID extends ValueObject { toPrimitive() { return this.toString(); } + + toBuffer(): Buffer { + return uuidStringToBinary(this.toString()); + } } diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index e27021a9..d034ab8f 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-logger", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-logger/src/strategies/console-logger.ts b/packages/rdx-logger/src/strategies/console-logger.ts index 5576037e..05ba3f20 100644 --- a/packages/rdx-logger/src/strategies/console-logger.ts +++ b/packages/rdx-logger/src/strategies/console-logger.ts @@ -1,4 +1,4 @@ -import { ILogger } from "../types"; +import type { ILogger } from "../types"; export class ConsoleLogger implements ILogger { info(message: string, meta?: any) { diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index de09085c..080a9e28 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-utils", - "version": "0.3.6", + "version": "0.4.7", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-utils/src/helpers/id-utils.ts b/packages/rdx-utils/src/helpers/id-utils.ts index efe232a2..8044d115 100644 --- a/packages/rdx-utils/src/helpers/id-utils.ts +++ b/packages/rdx-utils/src/helpers/id-utils.ts @@ -1,4 +1,31 @@ -import { v4 as uuidv4, v7 as uuidv7 } from "uuid"; +import { + parse as uuidParse, + stringify as uuidStringify, + v4 as uuidv4, + v7 as uuidv7, + validate, + version, +} from "uuid"; export const generateUUIDv4 = (): string => uuidv4(); export const generateUUIDv7 = (): string => uuidv7(); + +export function uuidStringToBinary(uuid: string): Buffer { + return Buffer.from(uuidParse(uuid)); +} + +export function uuidBinaryToString(buffer: Buffer): string { + return uuidStringify(buffer); +} + +export function isUuidValid(value: string): boolean { + return validate(value) && version(value) === 7; +} + +export function isUuidBinary(value: unknown): value is Buffer { + return Buffer.isBuffer(value) && value.length === 16; +} + +export function isUuidString(value: unknown): value is string { + return typeof value === "string" && isUuidValid(value); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8adebd70..75a63c70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@repo/rdx-logger': specifier: workspace:* version: link:../../packages/rdx-logger + '@repo/rdx-utils': + specifier: workspace:* + version: link:../../packages/rdx-utils bcrypt: specifier: ^5.1.1 version: 5.1.1 diff --git a/scripts/stacks/rodax/docker-compose.yml b/scripts/stacks/rodax/docker-compose.yml index e20275ef..aad5f5e9 100644 --- a/scripts/stacks/rodax/docker-compose.yml +++ b/scripts/stacks/rodax/docker-compose.yml @@ -93,7 +93,6 @@ services: DOCUMENTS_PATH: ${DOCUMENTS_PATH} SIGNED_DOCUMENTS_PATH: ${SIGNED_DOCUMENTS_PATH} - SIGNED_DOCUMENTS_CACHE_PATH: ${SIGNED_DOCUMENTS_CACHE_PATH} SIGNING_SERVICE_URL: ${SIGNING_SERVICE_URL} SIGNING_SERVICE_METHOD: ${SIGNING_SERVICE_METHOD} diff --git a/scripts/stacks/rodax/rodax.env b/scripts/stacks/rodax/rodax.env index 78b4cd38..a3737fbd 100644 --- a/scripts/stacks/rodax/rodax.env +++ b/scripts/stacks/rodax/rodax.env @@ -4,6 +4,22 @@ COMPANY_SLUG=rodax # Dominios DOMAIN=factuges.rodax-software.local +# API +PORT=3002 +API_BASE_PATH=/api/v1 +FRONTEND_URL=https://factuges.rodax-software.local +TRUST_PROXY=0 +TIMEZONE=Europe/Madrid +API_IMAGE=factuges-server:rodax-latest + +JWT_SECRET=supersecretkeysupersecretkeysupersecretkey +JWT_ACCESS_EXPIRATION=1h +JWT_REFRESH_EXPIRATION=7d + +WARMUP_TIMEOUT_MS=10000 +WARMUP_STRICT=false + + # MariaDB DB_HOST=db DB_PORT=3306 @@ -13,11 +29,10 @@ DB_USER=rodax_usr DB_PASS=supersecret DB_ROOT_PASS=verysecret +DB_LOGGING=true +DB_SSL=false +DB_SYNC_MODE=alter -# API -API_PORT=3002 -API_IMAGE=factuges-server:rodax-latest -FRONTEND_URL=factuges.rodax-software.local # Plantillas TEMPLATES_PATH=/shared/templates @@ -28,23 +43,16 @@ DOCUMENTS_PATH=/shared/documents # Firma de documentos SIGNED_DOCUMENTS_PATH=/shared/documents -SIGNED_DOCUMENTS_CACHE_PATH=/shared/cache SIGNING_SERVICE_URL=http://signing-service:8000/documents/sign SIGNING_SERVICE_METHOD=POST -SIGNING_SERVICE_TIMEOUT_MS=15_000 +SIGNING_SERVICE_TIMEOUT_MS=15000 SIGNING_SERVICE_MAX_RETRIES=2 -COMPANY_CERTIFICATES_JSON='{ - "rodax": { - "certificateId": "no se que poner aqui", - "certificateSecretName": "certificate_secret_name", - "certificatePasswordSecretName": "certificate_password_secret_name" - } -}' +COMPANY_CERTIFICATES_PATH=/shared/company-certificates.json # SYNC -ENV = development +ENV = production LOCAL_TZ = Europe/Madrid STATE_PATH = /app/state SYNC_MODE = all diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator index 6f7fad53..cf2e2f66 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe index abda1034..d34d274a 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe differ