v0.4.7
This commit is contained in:
parent
3a1c7e0844
commit
71b98fb6c2
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "WEB: Vite (Chrome)",
|
"name": "WEB: Vite (Chrome)",
|
||||||
|
|||||||
29
Dockerfile
29
Dockerfile
@ -44,6 +44,35 @@ RUN mkdir -p /usr/share/fonts/truetype/barlow \
|
|||||||
&& fc-cache -f -v
|
&& 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
|
# Reducir tamaño de la imagen
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
RUN rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { Account, AccountStatus } from "@/contexts/accounts/domain/";
|
import { Account, AccountStatus } from "@/contexts/accounts/domain/";
|
||||||
import {
|
import {
|
||||||
EmailAddress,
|
EmailAddress,
|
||||||
@ -11,8 +13,8 @@ import {
|
|||||||
type MapperParamsType,
|
type MapperParamsType,
|
||||||
SequelizeMapper,
|
SequelizeMapper,
|
||||||
} from "@/core/common/infrastructure/sequelize/sequelize-mapper";
|
} 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
|
export interface IAccountMapper
|
||||||
extends ISequelizeMapper<AccountModel, AccountCreationAttributes, Account> {}
|
extends ISequelizeMapper<AccountModel, AccountCreationAttributes, Account> {}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-server",
|
"name": "@erp/factuges-server",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||||
@ -38,6 +38,7 @@
|
|||||||
"@erp/customer-invoices": "workspace:*",
|
"@erp/customer-invoices": "workspace:*",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
"@repo/rdx-logger": "workspace:*",
|
"@repo/rdx-logger": "workspace:*",
|
||||||
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cls-rtracer": "^2.6.3",
|
"cls-rtracer": "^2.6.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import express, { type Application } from "express";
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import responseTime from "response-time";
|
import responseTime from "response-time";
|
||||||
|
|
||||||
|
import type { ConfigType } from "./config";
|
||||||
|
|
||||||
// ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config.
|
// ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config.
|
||||||
// dotenv.config();
|
// dotenv.config();
|
||||||
import { ENV } from "./config";
|
|
||||||
import { logger } from "./lib/logger";
|
import { logger } from "./lib/logger";
|
||||||
|
|
||||||
export function createApp(): Application {
|
export function createApp(config: ConfigType): Application {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
@ -30,7 +32,7 @@ export function createApp(): Application {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prodCors: CorsOptions = {
|
const prodCors: CorsOptions = {
|
||||||
origin: ENV.FRONTEND_URL,
|
origin: config.server.frontendUrl,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
exposedHeaders: [
|
exposedHeaders: [
|
||||||
@ -44,10 +46,10 @@ export function createApp(): Application {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors));
|
app.use(cors(config.flags.isDev ? devCors : prodCors));
|
||||||
app.options("*", cors(ENV.NODE_ENV === "development" ? 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
|
// Oculta la cabecera x-powered-by
|
||||||
app.disable("x-powered-by");
|
app.disable("x-powered-by");
|
||||||
|
|||||||
12
apps/server/src/config/company-certificates.json
Normal file
12
apps/server/src/config/company-certificates.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
154
apps/server/src/config/config.ts
Normal file
154
apps/server/src/config/config.ts
Normal file
@ -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<string, unknown>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@ -2,14 +2,14 @@ import { Sequelize } from "sequelize";
|
|||||||
|
|
||||||
import { logger } from "../lib/logger";
|
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),
|
* 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.
|
* autentica la conexión y devuelve la instancia lista para usar.
|
||||||
*/
|
*/
|
||||||
export async function tryConnectToDatabase(): Promise<Sequelize> {
|
export async function tryConnectToDatabase(config: ConfigType): Promise<Sequelize> {
|
||||||
const sequelize = buildSequelize();
|
const sequelize = buildSequelize(config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
@ -49,12 +49,12 @@ export async function closeDatabase(sequelize: Sequelize): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSequelize(): Sequelize {
|
function buildSequelize(config: ConfigType): Sequelize {
|
||||||
const common = {
|
const common = {
|
||||||
logging: ENV.DB_LOGGING
|
logging: config.database.logging
|
||||||
? (msg: any) => logger.debug(String(msg), { label: "sequelize" })
|
? (msg: any) => logger.debug(String(msg), { label: "sequelize" })
|
||||||
: false,
|
: false,
|
||||||
timezone: ENV.APP_TIMEZONE,
|
timezone: config.server.timezone,
|
||||||
pool: {
|
pool: {
|
||||||
max: 10,
|
max: 10,
|
||||||
min: 0,
|
min: 0,
|
||||||
@ -62,27 +62,28 @@ function buildSequelize(): Sequelize {
|
|||||||
acquire: 30000,
|
acquire: 30000,
|
||||||
},
|
},
|
||||||
dialectOptions: {
|
dialectOptions: {
|
||||||
timezone: ENV.APP_TIMEZONE,
|
timezone: config.server.timezone,
|
||||||
},
|
},
|
||||||
} as const;
|
} 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)
|
// URL completa (recomendada p.ej. en Postgres/MariaDB)
|
||||||
return new Sequelize(ENV.DATABASE_URL, common);
|
return new Sequelize(ENV.DATABASE_URL, common);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Parámetros sueltos (asegurar requeridos mínimos)
|
// 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");
|
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");
|
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, {
|
return new Sequelize(config.database.name, config.database.user, config.database.password, {
|
||||||
host: ENV.DB_HOST,
|
host: config.database.host,
|
||||||
port: ENV.DB_PORT,
|
port: config.database.port,
|
||||||
dialect: ENV.DB_DIALECT,
|
dialect: config.database.dialect,
|
||||||
...common,
|
...common,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
12
apps/server/src/config/env-error.ts
Normal file
12
apps/server/src/config/env-error.ts
Normal file
@ -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,
|
||||||
|
}));
|
||||||
57
apps/server/src/config/env-schema.ts
Normal file
57
apps/server/src/config/env-schema.ts
Normal file
@ -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),
|
||||||
|
});
|
||||||
@ -1,96 +1 @@
|
|||||||
import dotenv from "dotenv";
|
export * from "./config";
|
||||||
|
|
||||||
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;
|
|
||||||
|
|||||||
48
apps/server/src/config/load-company-certificates.ts
Normal file
48
apps/server/src/config/load-company-certificates.ts
Normal file
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
apps/server/src/config/log-config.ts
Normal file
35
apps/server/src/config/log-config.ts
Normal file
@ -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("────────────────────────────────────────");
|
||||||
|
};
|
||||||
@ -5,24 +5,24 @@ import { DateTime } from "luxon";
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createApp } from "./app.ts";
|
import { createApp } from "./app.ts";
|
||||||
|
import { initConfig } from "./config";
|
||||||
import { tryConnectToDatabase } from "./config/database.ts";
|
import { tryConnectToDatabase } from "./config/database.ts";
|
||||||
import { ENV } from "./config/index.ts";
|
|
||||||
import { registerHealthRoutes } from "./health.ts";
|
import { registerHealthRoutes } from "./health.ts";
|
||||||
import { listRoutes, logger } from "./lib/index.ts";
|
import { listRoutes, logger } from "./lib/index.ts";
|
||||||
import { initModules } from "./lib/modules/index.ts";
|
import { initModules } from "./lib/modules/index.ts";
|
||||||
import { registerModules } from "./register-modules.ts";
|
import { registerModules } from "./register-modules.ts";
|
||||||
|
|
||||||
const API_BASE_PATH = "/api/v1";
|
|
||||||
|
|
||||||
z.config(z.locales.es());
|
z.config(z.locales.es());
|
||||||
|
|
||||||
|
const config = initConfig(logger);
|
||||||
|
|
||||||
// Guardamos información del estado del servidor
|
// Guardamos información del estado del servidor
|
||||||
export const currentState = {
|
export const currentState = {
|
||||||
launchedAt: DateTime.now(),
|
launchedAt: DateTime.now(),
|
||||||
appPath: process.cwd(),
|
appPath: process.cwd(),
|
||||||
hosts: "0.0.0.0",
|
hosts: "0.0.0.0",
|
||||||
port: ENV.API_PORT,
|
port: config.server.port,
|
||||||
environment: ENV.NODE_ENV,
|
environment: config.env,
|
||||||
connections: {} as Record<string, unknown>,
|
connections: {} as Record<string, unknown>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,11 +91,11 @@ const serverError = (error: NodeJS.ErrnoException) => {
|
|||||||
logger.error("⛔️ Server wasn't able to start properly.", {
|
logger.error("⛔️ Server wasn't able to start properly.", {
|
||||||
label: "serverError0",
|
label: "serverError0",
|
||||||
error,
|
error,
|
||||||
port: ENV.API_PORT,
|
port: config.database.port,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error.code === "EADDRINUSE") {
|
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 {
|
} else {
|
||||||
logger.error(error.message, { error, label: "serverError2" });
|
logger.error(error.message, { error, label: "serverError2" });
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ const serverConnection = (conn: any) => {
|
|||||||
// Cargar paquetes de la aplicación: customers, invoices, routes...
|
// Cargar paquetes de la aplicación: customers, invoices, routes...
|
||||||
registerModules();
|
registerModules();
|
||||||
|
|
||||||
const app = createApp();
|
const app = createApp(config);
|
||||||
|
|
||||||
// Crea el servidor HTTP
|
// Crea el servidor HTTP
|
||||||
const server = http.createServer(app);
|
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(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`);
|
||||||
logger.info(`Process PID: ${process.pid}`);
|
logger.info(`Process PID: ${process.pid}`);
|
||||||
|
|
||||||
// Mostrar variables de entorno
|
const database = await tryConnectToDatabase(config);
|
||||||
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();
|
|
||||||
|
|
||||||
// Lógica de inicialización de DB, si procede:
|
// Lógica de inicialización de DB, si procede:
|
||||||
// initStructure(sequelizeConn.connection);
|
// initStructure(sequelizeConn.connection);
|
||||||
// insertUsers();
|
// insertUsers();
|
||||||
|
|
||||||
// ➕ Rutas de salud disponibles desde el inicio del proceso
|
// ➕ 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({
|
await initModules({
|
||||||
env: ENV,
|
config,
|
||||||
app,
|
app,
|
||||||
database,
|
database,
|
||||||
baseRoutePath: API_BASE_PATH,
|
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,7 +262,7 @@ process.on("uncaughtException", async (error: Error) => {
|
|||||||
logger.info(`Server environment: ${currentState.environment}`);
|
logger.info(`Server environment: ${currentState.environment}`);
|
||||||
logger.info("To shut down your server, press <CTRL> + C at any time");
|
logger.info("To shut down your server, press <CTRL> + 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) {
|
} catch (error) {
|
||||||
// Arranque fallido → readiness sigue false
|
// Arranque fallido → readiness sigue false
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import expressListRoutes from "express-list-routes";
|
import expressListRoutes from "express-list-routes";
|
||||||
|
|
||||||
// Función para listar rutas
|
// Función para listar rutas
|
||||||
export function listRoutes(app, basePath = "") {
|
export function listRoutes(app) {
|
||||||
return expressListRoutes(app, { logger: false, prefix: basePath });
|
return expressListRoutes(app, { logger: false, prefix: "" });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { ModuleParams } from "@erp/core/api";
|
import type { ModuleParams } from "@erp/core/api";
|
||||||
|
|
||||||
import { ENV } from "../../config";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,22 +118,23 @@ export const initModels = async (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3) Sincronizar base de datos (según modo)
|
// 3) Sincronizar base de datos (según modo)
|
||||||
const nodeEnv = ENV.NODE_ENV ?? process.env.NODE_ENV ?? "development";
|
const syncModeEnv = params.database.syncMode; // "none" | "alter" | "force"
|
||||||
const syncModeEnv = (ENV as any).DB_SYNC_MODE ?? process.env.DB_SYNC_MODE; // "none" | "alter" | "force"
|
|
||||||
const defaultMode = nodeEnv === "production" ? "none" : "alter";
|
logger.info(`ℹ️ Database sync mode ${syncModeEnv}`, { label: "initModels" });
|
||||||
const mode = (syncModeEnv ?? defaultMode) as "none" | "alter" | "force";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mode === "none") {
|
if (syncModeEnv === "none") {
|
||||||
logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" });
|
logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" });
|
||||||
} else if (mode === "alter") {
|
} else if (syncModeEnv === "alter") {
|
||||||
await database.sync({ force: false, alter: true });
|
await database.sync({ force: false, alter: true });
|
||||||
logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" });
|
logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" });
|
||||||
} else if (mode === "force") {
|
} else if (syncModeEnv === "force") {
|
||||||
await database.sync({ force: true, alter: false });
|
await database.sync({ force: true, alter: false });
|
||||||
logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" });
|
logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" });
|
||||||
} else {
|
} 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) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
|
|||||||
Binary file not shown.
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-web",
|
"name": "@erp/factuges-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host --clearScreen false",
|
"dev": "vite --host --clearScreen false",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/auth",
|
"name": "@erp/auth",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/core",
|
"name": "@erp/core",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -6,5 +6,5 @@ export interface IDocumentMetadata {
|
|||||||
readonly format: "PDF" | "HTML";
|
readonly format: "PDF" | "HTML";
|
||||||
readonly languageCode: string;
|
readonly languageCode: string;
|
||||||
readonly filename: string;
|
readonly filename: string;
|
||||||
readonly cacheKey?: string; // opcional
|
readonly storageKey?: string; // opcional
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(":");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<IDocument | null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guarda un documento firmado en cache.
|
|
||||||
* - best-effort
|
|
||||||
* - errores se ignoran o se loguean
|
|
||||||
*/
|
|
||||||
set(cacheKey: string, document: IDocument): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -48,9 +48,9 @@ export class DocumentGenerationService<TSnapshot> {
|
|||||||
// 2. Pre-processors (cache / short-circuit)
|
// 2. Pre-processors (cache / short-circuit)
|
||||||
for (const preProcessor of this.preProcessors) {
|
for (const preProcessor of this.preProcessors) {
|
||||||
try {
|
try {
|
||||||
const cached = await preProcessor.tryResolve(metadata);
|
const cachedDoc = await preProcessor.tryResolve(metadata);
|
||||||
if (cached) {
|
if (cachedDoc) {
|
||||||
return Result.ok(cached);
|
return Result.ok(cachedDoc);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// best-effort: ignorar y continuar
|
// best-effort: ignorar y continuar
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
|
export class DocumentStorageKeyFactory {
|
||||||
|
static fromMetadataRecord(metadata: Record<string, unknown>): string {
|
||||||
|
return createHash("sha256").update(JSON.stringify(metadata)).digest("hex");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,24 @@
|
|||||||
import type { IDocument } from "../application-models";
|
import type { IDocument } from "../application-models";
|
||||||
|
|
||||||
export interface IDocumentStorage {
|
export interface IDocumentStorage {
|
||||||
|
/**
|
||||||
|
* Determina si un existe una clave
|
||||||
|
*
|
||||||
|
* - Side-effect
|
||||||
|
* - Best-effort
|
||||||
|
* - Nunca lanza (errores se gestionan internamente)
|
||||||
|
*/
|
||||||
|
existsKeyStorage(storageKey: string): Promise<Boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera un documento guardado.
|
||||||
|
*
|
||||||
|
* - Side-effect
|
||||||
|
* - Best-effort
|
||||||
|
* - Nunca lanza (errores se gestionan internamente)
|
||||||
|
*/
|
||||||
|
readDocument(storageKey: string): Promise<IDocument | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persiste un documento generado.
|
* Persiste un documento generado.
|
||||||
*
|
*
|
||||||
@ -8,5 +26,5 @@ export interface IDocumentStorage {
|
|||||||
* - Best-effort
|
* - Best-effort
|
||||||
* - Nunca lanza (errores se gestionan internamente)
|
* - Nunca lanza (errores se gestionan internamente)
|
||||||
*/
|
*/
|
||||||
save(document: IDocument, metadata: Record<string, unknown>): Promise<void>;
|
saveDocument(document: IDocument, metadataRecord: Record<string, unknown>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
export * from "./document-cache.interface";
|
|
||||||
export * from "./document-cache-key-factory";
|
|
||||||
export * from "./document-generation-error";
|
export * from "./document-generation-error";
|
||||||
export * from "./document-generation-service";
|
export * from "./document-generation-service";
|
||||||
export * from "./document-metadata-factory.interface";
|
export * from "./document-metadata-factory.interface";
|
||||||
@ -10,4 +8,5 @@ export * from "./document-renderer.interface";
|
|||||||
export * from "./document-side-effect.interface";
|
export * from "./document-side-effect.interface";
|
||||||
export * from "./document-signing-service.interface";
|
export * from "./document-signing-service.interface";
|
||||||
export * from "./document-storage.interface";
|
export * from "./document-storage.interface";
|
||||||
|
export * from "./document-storage-key-factory";
|
||||||
export * from "./signing-context-resolver.interface";
|
export * from "./signing-context-resolver.interface";
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
export type BaseConfigType = {
|
||||||
|
env: string;
|
||||||
|
|
||||||
|
flags: {
|
||||||
|
isProd: boolean;
|
||||||
|
isDev: boolean;
|
||||||
|
isTest: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
1
modules/core/src/api/infrastructure/config/index.ts
Normal file
1
modules/core/src/api/infrastructure/config/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./base-config";
|
||||||
@ -1,42 +1,37 @@
|
|||||||
|
import type { ModuleParams } from "../../modules";
|
||||||
import {
|
import {
|
||||||
EnvCompanySigningContextResolver,
|
EnvCompanySigningContextResolver,
|
||||||
FastReportExecutableResolver,
|
FastReportExecutableResolver,
|
||||||
FastReportProcessRunner,
|
FastReportProcessRunner,
|
||||||
FastReportRenderer,
|
FastReportRenderer,
|
||||||
FastReportTemplateResolver,
|
FastReportTemplateResolver,
|
||||||
FilesystemDocumentCacheStore,
|
FilesystemDocumentStorage,
|
||||||
RestDocumentSigningService,
|
RestDocumentSigningService,
|
||||||
} from "../documents";
|
} from "../documents";
|
||||||
import { FilesystemDocumentStorage } from "../storage";
|
|
||||||
|
|
||||||
export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
|
export const buildCoreDocumentsDI = (params: ModuleParams) => {
|
||||||
const { TEMPLATES_PATH } = env;
|
const {
|
||||||
|
config: { paths, signingService },
|
||||||
|
} = params;
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
const frExecutableResolver = new FastReportExecutableResolver(env.FASTREPORT_BIN);
|
const frExecutableResolver = new FastReportExecutableResolver(paths.fastReportBin);
|
||||||
const frProcessRunner = new FastReportProcessRunner();
|
const frProcessRunner = new FastReportProcessRunner();
|
||||||
const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner);
|
const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner);
|
||||||
const fastReportTemplateResolver = new FastReportTemplateResolver(TEMPLATES_PATH!);
|
const fastReportTemplateResolver = new FastReportTemplateResolver(paths.templates);
|
||||||
|
|
||||||
// Signing
|
// Signing
|
||||||
const signingContextResolver = new EnvCompanySigningContextResolver(env);
|
const signingContextResolver = new EnvCompanySigningContextResolver(params);
|
||||||
|
|
||||||
const signingService = new RestDocumentSigningService({
|
const restSigningService = new RestDocumentSigningService({
|
||||||
signUrl: String(env.SIGNING_SERVICE_URL),
|
signUrl: String(signingService.url),
|
||||||
method: String(env.SIGNING_SERVICE_METHOD),
|
method: String(signingService.method),
|
||||||
timeoutMs: env.SIGNING_SERVICE_TIMEOUT_MS
|
timeoutMs: signingService.timeoutMs,
|
||||||
? Number.parseInt(env.SIGNING_SERVICE_TIMEOUT_MS, 10)
|
maxRetries: signingService.timeoutMs.maxRetries,
|
||||||
: 15_000,
|
|
||||||
maxRetries: env.SIGNING_SERVICE_MAX_RETRIES
|
|
||||||
? Number.parseInt(env.SIGNING_SERVICE_MAX_RETRIES, 10)
|
|
||||||
: 2,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cache para documentos firmados
|
|
||||||
const cacheStore = new FilesystemDocumentCacheStore(String(env.SIGNED_DOCUMENTS_CACHE_PATH));
|
|
||||||
|
|
||||||
// Almancenamiento para documentos firmados
|
// Almancenamiento para documentos firmados
|
||||||
const storage = new FilesystemDocumentStorage(String(env.SIGNED_DOCUMENTS_PATH));
|
const storage = new FilesystemDocumentStorage(String(paths.signedDocuments));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documentRenderers: {
|
documentRenderers: {
|
||||||
@ -44,11 +39,10 @@ export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
|
|||||||
fastReportTemplateResolver,
|
fastReportTemplateResolver,
|
||||||
},
|
},
|
||||||
documentSigning: {
|
documentSigning: {
|
||||||
signingService,
|
signingService: restSigningService,
|
||||||
signingContextResolver,
|
signingContextResolver,
|
||||||
},
|
},
|
||||||
documentStorage: {
|
documentStorage: {
|
||||||
cacheStore,
|
|
||||||
storage,
|
storage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
ICompanyCertificateContext,
|
ICompanyCertificateContext,
|
||||||
ISigningContextResolver,
|
ISigningContextResolver,
|
||||||
} from "@erp/core/api/application";
|
} from "@erp/core/api/application";
|
||||||
|
import type { ModuleParams } from "@erp/core/api/modules";
|
||||||
|
|
||||||
import type { ICompanySigningContextRecord } from "./company-signing-context-record.interface";
|
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 {
|
export class EnvCompanySigningContextResolver implements ISigningContextResolver {
|
||||||
private readonly records: Record<string, ICompanySigningContextRecord>;
|
private readonly records: Record<string, ICompanySigningContextRecord>;
|
||||||
|
|
||||||
constructor(env: NodeJS.ProcessEnv) {
|
constructor(params: ModuleParams) {
|
||||||
const raw = env.COMPANY_CERTIFICATES_JSON;
|
const jsonData = params.config.certificates as Record<
|
||||||
|
string,
|
||||||
if (!raw) {
|
{
|
||||||
this.records = {};
|
certificateId: string;
|
||||||
return;
|
certificateSecretName: string;
|
||||||
}
|
certificatePasswordSecretName: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw) as Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
certificateId: string;
|
|
||||||
certificateSecretName: string;
|
|
||||||
certificatePasswordSecretName: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
this.records = Object.fromEntries(
|
this.records = Object.fromEntries(
|
||||||
Object.entries(parsed).map(([companySlug, cfg]) => [
|
Object.entries(jsonData).map(([companySlug, cfg]) => [
|
||||||
companySlug,
|
companySlug,
|
||||||
{
|
{
|
||||||
certificateId: cfg.certificateId,
|
certificateId: cfg.certificateId,
|
||||||
|
|||||||
@ -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<IDocument | null> {
|
|
||||||
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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<Boolean> {
|
||||||
|
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<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string, unknown>): Promise<void> {
|
||||||
|
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, unknown>): 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./filesystem-document-cache-store";
|
export * from "./filesystem-signed-document-storage";
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./config";
|
||||||
export * from "./database";
|
export * from "./database";
|
||||||
export * from "./di";
|
export * from "./di";
|
||||||
export * from "./documents";
|
export * from "./documents";
|
||||||
@ -6,4 +7,3 @@ export * from "./express";
|
|||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
export * from "./storage";
|
|
||||||
|
|||||||
@ -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<string, unknown>): Promise<void> {
|
|
||||||
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, unknown>): 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./filesystem-signed-document-storage";
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customer-invoices",
|
"name": "@erp/customer-invoices",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -29,17 +29,23 @@ export class IssuedInvoiceDocumentMetadataFactory
|
|||||||
format: "PDF",
|
format: "PDF",
|
||||||
languageCode: snapshot.language_code ?? "es",
|
languageCode: snapshot.language_code ?? "es",
|
||||||
filename: this.buildFilename(snapshot),
|
filename: this.buildFilename(snapshot),
|
||||||
cacheKey: this.buildCacheKey(snapshot),
|
storageKey: this.buildCacheKey(snapshot),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string {
|
private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string {
|
||||||
// Ejemplo: factura-F2024-000123.pdf
|
// Ejemplo: factura-F2024-000123-FULANITO.pdf
|
||||||
return `factura-${snapshot.invoice_number}.pdf`;
|
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string {
|
private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string {
|
||||||
// Versionado explícito para invalidaciones futuras
|
// 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(":");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { buildCoreDocumentsDI } from "@erp/core/api";
|
import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IssuedInvoiceDocumentPipelineFactory,
|
IssuedInvoiceDocumentPipelineFactory,
|
||||||
type IssuedInvoiceDocumentPipelineFactoryDeps,
|
type IssuedInvoiceDocumentPipelineFactoryDeps,
|
||||||
} from "../documents";
|
} from "../documents";
|
||||||
|
|
||||||
export const buildIssuedInvoiceDocumentService = (env: NodeJS.ProcessEnv) => {
|
export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => {
|
||||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env);
|
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
|
||||||
|
|
||||||
const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = {
|
const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = {
|
||||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export type IssuedInvoicesInternalDeps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesInternalDeps {
|
export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesInternalDeps {
|
||||||
const { database, env } = params;
|
const { database } = params;
|
||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
const transactionManager = buildTransactionManager(database);
|
const transactionManager = buildTransactionManager(database);
|
||||||
@ -34,7 +34,7 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv
|
|||||||
// Application helpers
|
// Application helpers
|
||||||
const finder = buildIssuedInvoiceFinder(repository);
|
const finder = buildIssuedInvoiceFinder(repository);
|
||||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(env);
|
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params);
|
||||||
|
|
||||||
// Internal use cases (factories)
|
// Internal use cases (factories)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
DocumentPostProcessorChain,
|
DocumentPostProcessorChain,
|
||||||
type FastReportRenderer,
|
type FastReportRenderer,
|
||||||
type FastReportTemplateResolver,
|
type FastReportTemplateResolver,
|
||||||
type IDocumentCacheStore,
|
|
||||||
type IDocumentPostProcessor,
|
type IDocumentPostProcessor,
|
||||||
type IDocumentSideEffect,
|
type IDocumentSideEffect,
|
||||||
type IDocumentSigningService,
|
type IDocumentSigningService,
|
||||||
@ -34,7 +33,6 @@ export interface IssuedInvoiceDocumentPipelineFactoryDeps {
|
|||||||
signingContextResolver: ISigningContextResolver;
|
signingContextResolver: ISigningContextResolver;
|
||||||
documentSigningService: IDocumentSigningService;
|
documentSigningService: IDocumentSigningService;
|
||||||
|
|
||||||
documentCacheStore: IDocumentCacheStore;
|
|
||||||
documentStorage: IDocumentStorage;
|
documentStorage: IDocumentStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,9 +41,7 @@ export class IssuedInvoiceDocumentPipelineFactory {
|
|||||||
deps: IssuedInvoiceDocumentPipelineFactoryDeps
|
deps: IssuedInvoiceDocumentPipelineFactoryDeps
|
||||||
): IssuedInvoiceDocumentGeneratorService {
|
): IssuedInvoiceDocumentGeneratorService {
|
||||||
// 1. Pre-processors (cache firmado)
|
// 1. Pre-processors (cache firmado)
|
||||||
const preProcessors = [
|
const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)];
|
||||||
new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentCacheStore),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. Renderer (FastReport)
|
// 2. Renderer (FastReport)
|
||||||
const documentRenderer = new IssuedInvoiceDocumentRenderer(
|
const documentRenderer = new IssuedInvoiceDocumentRenderer(
|
||||||
|
|||||||
@ -28,7 +28,9 @@ export class DigitalSignaturePostProcessor implements IDocumentPostProcessor {
|
|||||||
// 1. Resolver certificado de la empresa
|
// 1. Resolver certificado de la empresa
|
||||||
const certificate = await this.certificateResolver.resolveForCompany(metadata.companySlug);
|
const certificate = await this.certificateResolver.resolveForCompany(metadata.companySlug);
|
||||||
if (!certificate) {
|
if (!certificate) {
|
||||||
throw new Error("[DigitalSignaturePostProcessor] Compny certificate is undefined");
|
throw new Error(
|
||||||
|
`[DigitalSignaturePostProcessor] Company certificate is undefined for ${metadata.companySlug}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Firmar payload
|
// 2. Firmar payload
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
DocumentCacheKeyFactory,
|
DocumentStorageKeyFactory,
|
||||||
type IDocument,
|
type IDocument,
|
||||||
type IDocumentCacheStore,
|
|
||||||
type IDocumentMetadata,
|
type IDocumentMetadata,
|
||||||
type IDocumentPreProcessor,
|
type IDocumentPreProcessor,
|
||||||
|
type IDocumentStorage,
|
||||||
|
logger,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,42 +16,35 @@ import {
|
|||||||
* - Invalida cache corrupto
|
* - Invalida cache corrupto
|
||||||
*/
|
*/
|
||||||
export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPreProcessor {
|
export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPreProcessor {
|
||||||
constructor(private readonly cache: IDocumentCacheStore) {}
|
constructor(private readonly docStorage: IDocumentStorage) {}
|
||||||
|
|
||||||
async tryResolve(metadata: IDocumentMetadata): Promise<IDocument | null> {
|
async tryResolve(metadata: IDocumentMetadata): Promise<IDocument | null> {
|
||||||
if (!metadata.cacheKey) {
|
const metadataRecord = metadata as unknown as Record<string, unknown>;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheKey = DocumentCacheKeyFactory.fromMetadata(metadata);
|
const storageKey = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord);
|
||||||
|
|
||||||
return await this.cache.get(cacheKey);
|
if (!storageKey) {
|
||||||
} catch {
|
return null;
|
||||||
// best-effort: cualquier fallo se trata como cache miss
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadata.cacheKey) {
|
const exists = await this.docStorage.existsKeyStorage(storageKey);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const exists = await this.cache.exists(metadata.cacheKey);
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const document = await this.cache.read(metadata.cacheKey);
|
const document = await this.docStorage.readDocument(storageKey);
|
||||||
|
|
||||||
if (!this.isValid(document)) {
|
if (!this.isValid(document)) {
|
||||||
await this.cache.invalidate(metadata.cacheKey);
|
logger.warn(`Storage key ${storageKey} not exists!`, {
|
||||||
|
lable: "IssuedInvoiceSignedDocumentCachePreProcessor",
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
} catch {
|
} catch {
|
||||||
// Cache failure → ignore and continue pipeline
|
// best-effort: cualquier fallo se trata como cache miss
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +53,9 @@ export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPr
|
|||||||
* Validación mínima de integridad.
|
* Validación mínima de integridad.
|
||||||
* No valida firma criptográfica.
|
* 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) {
|
if (!document.payload || document.payload.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,11 @@ export class PersistIssuedInvoiceDocumentSideEffect implements IDocumentSideEffe
|
|||||||
|
|
||||||
async execute(document: IDocument, metadata: IDocumentMetadata): Promise<void> {
|
async execute(document: IDocument, metadata: IDocumentMetadata): Promise<void> {
|
||||||
// Si no hay cacheKey, no se persiste
|
// Si no hay cacheKey, no se persiste
|
||||||
if (!metadata.cacheKey) {
|
if (!metadata.storageKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistencia best-effort
|
// Persistencia best-effort
|
||||||
await this.storage.save(document, metadata);
|
await this.storage.saveDocument(document, metadata as unknown as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||||
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
||||||
import type { ILogger } from "@repo/rdx-logger";
|
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
|
||||||
import type { Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GetIssueInvoiceByIdRequestSchema,
|
GetIssueInvoiceByIdRequestSchema,
|
||||||
@ -17,12 +15,7 @@ import { ListIssuedInvoicesController } from "./controllers/list-issued-invoices
|
|||||||
import { ReportIssuedInvoiceController } from "./controllers/report-issued-invoice.controller";
|
import { ReportIssuedInvoiceController } from "./controllers/report-issued-invoice.controller";
|
||||||
|
|
||||||
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
|
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
|
||||||
const { app, baseRoutePath, logger } = params as {
|
const { app, config } = params;
|
||||||
app: Application;
|
|
||||||
database: Sequelize;
|
|
||||||
baseRoutePath: string;
|
|
||||||
logger: ILogger;
|
|
||||||
};
|
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: true });
|
const router: Router = Router({ mergeParams: true });
|
||||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||||
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
||||||
import type { ILogger } from "@repo/rdx-logger";
|
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
|
||||||
import type { Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeStatusProformaByIdParamsRequestSchema,
|
ChangeStatusProformaByIdParamsRequestSchema,
|
||||||
@ -17,7 +15,7 @@ import {
|
|||||||
UpdateProformaByIdParamsRequestSchema,
|
UpdateProformaByIdParamsRequestSchema,
|
||||||
UpdateProformaByIdRequestSchema,
|
UpdateProformaByIdRequestSchema,
|
||||||
} from "../../../../common";
|
} from "../../../../common";
|
||||||
import { buildProformasDependencies } from "../../proformas-dependencies";
|
import { buildProformasDependencies } from "../../di/proformas.di";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeStatusProformaController,
|
ChangeStatusProformaController,
|
||||||
@ -31,13 +29,7 @@ import {
|
|||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const proformasRouter = (params: ModuleParams) => {
|
export const proformasRouter = (params: ModuleParams) => {
|
||||||
const { app, baseRoutePath, logger } = params as {
|
const { app, config } = params;
|
||||||
env: Record<string, any>;
|
|
||||||
app: Application;
|
|
||||||
database: Sequelize;
|
|
||||||
baseRoutePath: string;
|
|
||||||
logger: ILogger;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deps = buildProformasDependencies(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);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customers",
|
"name": "@erp/customers",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/doc-numbering",
|
"name": "@erp/doc-numbering",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -34,5 +34,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24"
|
"node": ">=24"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.20.0"
|
"packageManager": "pnpm@10.29.3"
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-criteria",
|
"name": "@repo/rdx-criteria",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-ddd",
|
"name": "@repo/rdx-ddd",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -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 { z } from "zod/v4";
|
||||||
|
|
||||||
import { translateZodValidationError } from "../helpers";
|
import { translateZodValidationError } from "../helpers";
|
||||||
|
|
||||||
import { ValueObject } from "./value-object";
|
import { ValueObject } from "./value-object";
|
||||||
|
|
||||||
export class UniqueID extends ValueObject<string> {
|
export class UniqueID extends ValueObject<string> {
|
||||||
@ -9,8 +17,10 @@ export class UniqueID extends ValueObject<string> {
|
|||||||
return schema.safeParse(value);
|
return schema.safeParse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(id?: string, generateOnEmpty = false): Result<UniqueID, Error> {
|
static create(id?: string | Buffer, generateOnEmpty = false): Result<UniqueID, Error> {
|
||||||
if (!id || id?.trim() === "") {
|
const _id = isUuidBinary(id) ? uuidBinaryToString(id) : id;
|
||||||
|
|
||||||
|
if (!_id || _id?.trim() === "") {
|
||||||
if (!generateOnEmpty) {
|
if (!generateOnEmpty) {
|
||||||
return Result.fail(new Error("ID cannot be undefined or null"));
|
return Result.fail(new Error("ID cannot be undefined or null"));
|
||||||
}
|
}
|
||||||
@ -18,7 +28,7 @@ export class UniqueID extends ValueObject<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
const valueIsValid = UniqueID.validate(id!);
|
const valueIsValid = UniqueID.validate(_id!);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
if (!valueIsValid.success) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
@ -44,4 +54,8 @@ export class UniqueID extends ValueObject<string> {
|
|||||||
toPrimitive() {
|
toPrimitive() {
|
||||||
return this.toString();
|
return this.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toBuffer(): Buffer {
|
||||||
|
return uuidStringToBinary(this.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-logger",
|
"name": "@repo/rdx-logger",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ILogger } from "../types";
|
import type { ILogger } from "../types";
|
||||||
|
|
||||||
export class ConsoleLogger implements ILogger {
|
export class ConsoleLogger implements ILogger {
|
||||||
info(message: string, meta?: any) {
|
info(message: string, meta?: any) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-utils",
|
"name": "@repo/rdx-utils",
|
||||||
"version": "0.3.6",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -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 generateUUIDv4 = (): string => uuidv4();
|
||||||
export const generateUUIDv7 = (): string => uuidv7();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -53,6 +53,9 @@ importers:
|
|||||||
'@repo/rdx-logger':
|
'@repo/rdx-logger':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/rdx-logger
|
version: link:../../packages/rdx-logger
|
||||||
|
'@repo/rdx-utils':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-utils
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
|
|||||||
@ -93,7 +93,6 @@ services:
|
|||||||
DOCUMENTS_PATH: ${DOCUMENTS_PATH}
|
DOCUMENTS_PATH: ${DOCUMENTS_PATH}
|
||||||
|
|
||||||
SIGNED_DOCUMENTS_PATH: ${SIGNED_DOCUMENTS_PATH}
|
SIGNED_DOCUMENTS_PATH: ${SIGNED_DOCUMENTS_PATH}
|
||||||
SIGNED_DOCUMENTS_CACHE_PATH: ${SIGNED_DOCUMENTS_CACHE_PATH}
|
|
||||||
|
|
||||||
SIGNING_SERVICE_URL: ${SIGNING_SERVICE_URL}
|
SIGNING_SERVICE_URL: ${SIGNING_SERVICE_URL}
|
||||||
SIGNING_SERVICE_METHOD: ${SIGNING_SERVICE_METHOD}
|
SIGNING_SERVICE_METHOD: ${SIGNING_SERVICE_METHOD}
|
||||||
|
|||||||
@ -4,6 +4,22 @@ COMPANY_SLUG=rodax
|
|||||||
# Dominios
|
# Dominios
|
||||||
DOMAIN=factuges.rodax-software.local
|
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
|
# MariaDB
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
@ -13,11 +29,10 @@ DB_USER=rodax_usr
|
|||||||
DB_PASS=supersecret
|
DB_PASS=supersecret
|
||||||
DB_ROOT_PASS=verysecret
|
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
|
# Plantillas
|
||||||
TEMPLATES_PATH=/shared/templates
|
TEMPLATES_PATH=/shared/templates
|
||||||
@ -28,23 +43,16 @@ DOCUMENTS_PATH=/shared/documents
|
|||||||
|
|
||||||
# Firma de documentos
|
# Firma de documentos
|
||||||
SIGNED_DOCUMENTS_PATH=/shared/documents
|
SIGNED_DOCUMENTS_PATH=/shared/documents
|
||||||
SIGNED_DOCUMENTS_CACHE_PATH=/shared/cache
|
|
||||||
|
|
||||||
SIGNING_SERVICE_URL=http://signing-service:8000/documents/sign
|
SIGNING_SERVICE_URL=http://signing-service:8000/documents/sign
|
||||||
SIGNING_SERVICE_METHOD=POST
|
SIGNING_SERVICE_METHOD=POST
|
||||||
SIGNING_SERVICE_TIMEOUT_MS=15_000
|
SIGNING_SERVICE_TIMEOUT_MS=15000
|
||||||
SIGNING_SERVICE_MAX_RETRIES=2
|
SIGNING_SERVICE_MAX_RETRIES=2
|
||||||
|
|
||||||
COMPANY_CERTIFICATES_JSON='{
|
COMPANY_CERTIFICATES_PATH=/shared/company-certificates.json
|
||||||
"rodax": {
|
|
||||||
"certificateId": "no se que poner aqui",
|
|
||||||
"certificateSecretName": "certificate_secret_name",
|
|
||||||
"certificatePasswordSecretName": "certificate_password_secret_name"
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
# SYNC
|
# SYNC
|
||||||
ENV = development
|
ENV = production
|
||||||
LOCAL_TZ = Europe/Madrid
|
LOCAL_TZ = Europe/Madrid
|
||||||
STATE_PATH = /app/state
|
STATE_PATH = /app/state
|
||||||
SYNC_MODE = all
|
SYNC_MODE = all
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user