This commit is contained in:
David Arranz 2026-02-10 13:39:55 +01:00
parent 2b3dfce72c
commit 3a1c7e0844
47 changed files with 183 additions and 158 deletions

2
.vscode/launch.json vendored
View File

@ -1,5 +1,5 @@
{ {
"version": "0.2.7", "version": "0.3.6",
"configurations": [ "configurations": [
{ {
"name": "WEB: Vite (Chrome)", "name": "WEB: Vite (Chrome)",

View File

@ -1,53 +0,0 @@
# ───────────────────────────────
# Identidad de la compañía
# ───────────────────────────────
COMPANY=rodax
CTE_COMPANY_ID=5e4dc5b3-96b9-4968-9490-14bd032fec5f
# Dominios
DOMAIN=factuges.rodax-software.local
# ───────────────────────────────
# Base de datos (Sequelize / MySQL-MariaDB)
# ───────────────────────────────
DB_ROOT_PASS=verysecret
DB_USER=rodax_usr
DB_PASS=supersecret
DB_NAME=rodax_db
DB_PORT=3306
# Log de Sequelize (true|false)
DB_LOGGING=false
# Alterar estructura BD
DB_SYNC_MODE=none # none | alter | force
# ───────────────────────────────
# API
# ───────────────────────────────
API_PORT=3002
API_IMAGE=factuges-server:rodax-latest
# Plantillas
TEMPLATES_PATH=/repo/apps/server/templates
# Documentos generados
DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/documents
# Firma de documentos
SIGN_DOCUMENTS=false
SIGNED_DOCUMENTS_PATH=/home/rodax/Documentos/uecko-erp/out/rodax/signed-documents
# Chrome executable path (Puppeteer)
# PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome
# URL pública del frontend (CORS)
FRONTEND_URL=factuges.rodax-software.local
# Tiempo máximo para cada warmup() de un módulo, en milisegundos.
WARMUP_TIMEOUT_MS=10000
# Si es true, un fallo de warmup aborta el arranque. Si es false, continúa con warning.
WARMUP_STRICT=false

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/factuges-server", "name": "@erp/factuges-server",
"version": "0.2.7", "version": "0.3.6",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "tsup src/index.ts --config tsup.config.ts", "build": "tsup src/index.ts --config tsup.config.ts",

View File

@ -52,6 +52,14 @@ const DOCUMENTS_PATH = process.env.DOCUMENTS_PATH ?? "./documents";
// Proxy (no usáis ahora, pero dejamos la variable por si se activa en el futuro) // 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 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 = { export const ENV = {
NODE_ENV, NODE_ENV,
API_PORT, API_PORT,
@ -67,9 +75,18 @@ export const ENV = {
DB_SYNC_MODE, DB_SYNC_MODE,
APP_TIMEZONE, APP_TIMEZONE,
TRUST_PROXY, TRUST_PROXY,
TEMPLATES_PATH, TEMPLATES_PATH,
DOCUMENTS_PATH, DOCUMENTS_PATH,
FASTREPORT_BIN, FASTREPORT_BIN,
SIGNING_SERVICE_URL,
SIGNING_SERVICE_METHOD,
SIGNING_SERVICE_TIMEOUT_MS,
SIGNING_SERVICE_MAX_RETRIES,
COMPANY_SLUG,
COMPANY_CERTIFICATES_JSON,
} as const; } as const;
export const Flags = { export const Flags = {

View File

@ -215,6 +215,7 @@ process.on("uncaughtException", async (error: Error) => {
logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`); logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`);
logger.info(`DB_DIALECT: ${ENV.DB_DIALECT}`);
logger.info(`DB_HOST: ${ENV.DB_HOST}`); logger.info(`DB_HOST: ${ENV.DB_HOST}`);
logger.info(`DB_PORT: ${ENV.DB_PORT}`); logger.info(`DB_PORT: ${ENV.DB_PORT}`);
logger.info(`DB_NAME: ${ENV.DB_NAME}`); logger.info(`DB_NAME: ${ENV.DB_NAME}`);
@ -225,6 +226,9 @@ process.on("uncaughtException", async (error: Error) => {
logger.info(`FASTREPORT_BIN: ${ENV.FASTREPORT_BIN}`); logger.info(`FASTREPORT_BIN: ${ENV.FASTREPORT_BIN}`);
logger.info(`TEMPLATES_PATH: ${ENV.TEMPLATES_PATH}`); 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();
// Lógica de inicialización de DB, si procede: // Lógica de inicialización de DB, si procede:

View File

@ -0,0 +1,14 @@
{
"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"
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/factuges-web", "name": "@erp/factuges-web",
"private": true, "private": true,
"version": "0.2.7", "version": "0.3.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host --clearScreen false", "dev": "vite --host --clearScreen false",

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/auth", "name": "@erp/auth",
"version": "0.1.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/core", "name": "@erp/core",
"version": "0.1.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -2,6 +2,7 @@ export interface IDocumentMetadata {
readonly documentType: string; readonly documentType: string;
readonly documentId: string; readonly documentId: string;
readonly companyId: string; readonly companyId: string;
readonly companySlug: string;
readonly format: "PDF" | "HTML"; readonly format: "PDF" | "HTML";
readonly languageCode: string; readonly languageCode: string;
readonly filename: string; readonly filename: string;

View File

@ -76,6 +76,7 @@ export class DocumentGenerationService<TSnapshot> {
try { try {
document = await this.postProcessor.process(document, metadata); document = await this.postProcessor.process(document, metadata);
} catch (error) { } catch (error) {
console.error(error);
return Result.fail(DocumentGenerationError.postProcess(error)); return Result.fail(DocumentGenerationError.postProcess(error));
} }
} }

View File

@ -1,3 +1,4 @@
export * from "./documents"; export * from "./documents";
export * from "./presenters";
export * from "./renderers"; export * from "./renderers";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

@ -0,0 +1,4 @@
export * from "./presenter";
export * from "./presenter.interface";
export * from "./presenter-registry";
export * from "./presenter-registry.interface";

View File

@ -1,6 +1,6 @@
import type { DTO } from "@erp/core/common"; import type { DTO } from "@erp/core/common";
import type { ISnapshotBuilder } from "./snapshot-builder.interface"; import type { IPresenter } from "./presenter.interface";
/** /**
* 🔑 Claves de proyección comunes para seleccionar presenters * 🔑 Claves de proyección comunes para seleccionar presenters
@ -25,17 +25,17 @@ export interface IPresenterRegistry {
*/ */
getPresenter<TSource, F extends PresenterFormat = "DTO">( getPresenter<TSource, F extends PresenterFormat = "DTO">(
key: Omit<PresenterKey, "format"> & { format?: F } key: Omit<PresenterKey, "format"> & { format?: F }
): ISnapshotBuilder<TSource, PresenterFormatOutputMap[F]>; ): IPresenter<TSource, PresenterFormatOutputMap[F]>;
/** /**
* Registra un mapper de dominio bajo una clave de proyección. * Registra un mapper de dominio bajo una clave de proyección.
*/ */
registerPresenter<TSource, TOutput>( registerPresenter<TSource, TOutput>(
key: PresenterKey, key: PresenterKey,
presenter: ISnapshotBuilder<TSource, TOutput> presenter: IPresenter<TSource, TOutput>
): this; ): this;
registerPresenters( registerPresenters(
presenters: Array<{ key: PresenterKey; presenter: ISnapshotBuilder<unknown, unknown> }> presenters: Array<{ key: PresenterKey; presenter: IPresenter<unknown, unknown> }>
): this; ): this;
} }

View File

@ -1,10 +1,10 @@
import { ApplicationError } from "@repo/rdx-ddd"; import { ApplicationError } from "@repo/rdx-ddd";
import type { IPresenter } from "./presenter.interface";
import type { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface"; import type { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface";
import type { ISnapshotBuilder } from "./snapshot-builder.interface";
export class InMemoryPresenterRegistry implements IPresenterRegistry { export class InMemoryPresenterRegistry implements IPresenterRegistry {
private registry: Map<string, ISnapshotBuilder<any, any>> = new Map(); private registry: Map<string, IPresenter<any, any>> = new Map();
private _normalizeKey(key: PresenterKey): PresenterKey { private _normalizeKey(key: PresenterKey): PresenterKey {
return { return {
@ -29,13 +29,13 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry {
private _registerPresenter<TSource, TOutput>( private _registerPresenter<TSource, TOutput>(
key: PresenterKey, key: PresenterKey,
presenter: ISnapshotBuilder<TSource, TOutput> presenter: IPresenter<TSource, TOutput>
): void { ): void {
const exactKey = this._buildKey(key); const exactKey = this._buildKey(key);
this.registry.set(exactKey, presenter); this.registry.set(exactKey, presenter);
} }
getPresenter<TSource, TOutput>(key: PresenterKey): ISnapshotBuilder<TSource, TOutput> { getPresenter<TSource, TOutput>(key: PresenterKey): IPresenter<TSource, TOutput> {
const exactKey = this._buildKey(key); const exactKey = this._buildKey(key);
// 1) Intentar clave exacta // 1) Intentar clave exacta
@ -76,7 +76,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry {
registerPresenter<TSource, TOutput>( registerPresenter<TSource, TOutput>(
key: PresenterKey, key: PresenterKey,
presenter: ISnapshotBuilder<TSource, TOutput> presenter: IPresenter<TSource, TOutput>
): this { ): this {
this._registerPresenter(key, presenter); this._registerPresenter(key, presenter);
return this; return this;
@ -85,7 +85,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry {
* Registro en lote de presentadores. * Registro en lote de presentadores.
*/ */
registerPresenters( registerPresenters(
presenters: Array<{ key: PresenterKey; presenter: ISnapshotBuilder<any, any> }> presenters: Array<{ key: PresenterKey; presenter: IPresenter<any, any> }>
): this { ): this {
for (const { key, presenter } of presenters) { for (const { key, presenter } of presenters) {
this._registerPresenter(key, presenter); this._registerPresenter(key, presenter);

View File

@ -0,0 +1,7 @@
import type { DTO } from "../../../common/types";
export type IPresenterOutputParams = Record<string, unknown>;
export interface IPresenter<TSource, TOutput = DTO> {
toOutput(source: TSource, params?: IPresenterOutputParams): TOutput | Promise<TOutput>;
}

View File

@ -0,0 +1,11 @@
import type { IPresenter, IPresenterOutputParams } from "./presenter.interface";
import type { IPresenterRegistry } from "./presenter-registry.interface";
export abstract class Presenter<TSource = unknown, TOutput = unknown>
implements IPresenter<TSource, TOutput>
{
constructor(protected presenterRegistry: IPresenterRegistry) {
//
}
abstract toOutput(source: TSource, params?: IPresenterOutputParams): TOutput;
}

View File

@ -1,4 +1,2 @@
export * from "./presenter-registry";
export * from "./presenter-registry.interface";
export * from "./snapshot-builder"; export * from "./snapshot-builder";
export * from "./snapshot-builder.interface"; export * from "./snapshot-builder.interface";

View File

@ -1,4 +1,5 @@
import type { IPresenterRegistry } from "./presenter-registry.interface"; import type { IPresenterRegistry } from "../presenters";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "./snapshot-builder.interface"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "./snapshot-builder.interface";
export abstract class SnapshotBuilder<TSource = unknown, TOutput = unknown> export abstract class SnapshotBuilder<TSource = unknown, TOutput = unknown>

View File

@ -22,9 +22,14 @@ export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
const signingContextResolver = new EnvCompanySigningContextResolver(env); const signingContextResolver = new EnvCompanySigningContextResolver(env);
const signingService = new RestDocumentSigningService({ const signingService = new RestDocumentSigningService({
signUrl: String(env.SIGNING_BASE_URL), signUrl: String(env.SIGNING_SERVICE_URL),
timeoutMs: env.SIGNING_TIMEOUT_MS ? Number.parseInt(env.SIGNING_TIMEOUT_MS, 10) : 15_000, method: String(env.SIGNING_SERVICE_METHOD),
maxRetries: env.SIGNING_MAX_RETRIES ? Number.parseInt(env.SIGNING_MAX_RETRIES, 10) : 2, 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,
}); });
// Cache para documentos firmados // Cache para documentos firmados

View File

@ -52,20 +52,15 @@ export class EnvCompanySigningContextResolver implements ISigningContextResolver
} }
} }
async resolveForCompany(companyId: string): Promise<ICompanyCertificateContext | null> { async resolveForCompany(companySlug: string): Promise<ICompanyCertificateContext | null> {
/** const record = this.records[companySlug];
* En esta implementación:
* - companyId === companySlug
* - No hay lookup adicional
*/
const record = this.records[companyId];
if (!record) { if (!record) {
return null; return null;
} }
return { return {
companySlug: companyId, companySlug,
certificateSecretName: record.certificateSecretName, certificateSecretName: record.certificateSecretName,
certificatePasswordSecretName: record.certificatePasswordSecretName, certificatePasswordSecretName: record.certificatePasswordSecretName,
}; };

View File

@ -1,7 +1,7 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { Result, buildSafePath } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { FastReportExecutionError } from "./fastreport-errors"; import { FastReportExecutionError } from "./fastreport-errors";
@ -18,7 +18,7 @@ export type FastReportProcessRunnerArgs = {
templatePath: string; // Path to FRX template (required) templatePath: string; // Path to FRX template (required)
data: string; // JSON data as string data: string; // JSON data as string
format: "PDF" | "HTML"; format: "PDF" | "HTML";
workdir: string; // Directorio de trabajo temporal output: string; // Directorio de trabajo temporal
}; };
export class FastReportProcessRunner { export class FastReportProcessRunner {
@ -26,40 +26,40 @@ export class FastReportProcessRunner {
executablePath: string, executablePath: string,
executableArgs: FastReportProcessRunnerArgs executableArgs: FastReportProcessRunnerArgs
): Promise<Result<Buffer | string, FastReportExecutionError>> { ): Promise<Result<Buffer | string, FastReportExecutionError>> {
const { templatePath, data, format, workdir } = executableArgs; const { templatePath, data, format, output } = executableArgs;
// Guardar datos de entrada en JSON // Guardar datos de entrada en JSON
const dataPath = buildSafePath({ basePath: workdir, segments: [], filename: "data.json" }); //const dataPath = buildSafePath({ basePath: workdir, segments: [], filename: "data.json" });
// Path de output según formato y con // Path de output según formato y con
const outputPath = buildSafePath({ /*const outputPath = buildSafePath({
basePath: workdir, basePath: workdir,
segments: [], segments: [],
filename: format === "PDF" ? "output.pdf" : "output.html", filename: format === "PDF" ? "output.pdf" : "output.html",
}); });
await fs.writeFile(dataPath, data, "utf-8"); await fs.writeFile(dataPath, data, "utf-8");*/
const args = this.buildArgs({ const args = this.buildArgs({
templatePath, templatePath,
dataPath, data,
outputPath, output,
format, format,
}); });
return this.executeProcess(executablePath, args, outputPath, executableArgs.format); return this.executeProcess(executablePath, args, output, executableArgs.format);
} }
private buildArgs(params: { private buildArgs(params: {
templatePath: string; templatePath: string;
dataPath: string; data: string;
outputPath: string; output: string;
format: "PDF" | "HTML"; format: "PDF" | "HTML";
}): string[] { }): string[] {
return [ return [
`--template=${params.templatePath}`, `--template=${params.templatePath}`,
`--data=${params.dataPath}`, `--data=${params.data}`,
`--output=${params.outputPath}`, `--output=${params.output}`,
`--format=${params.format}`, `--format=${params.format}`,
]; ];
} }

View File

@ -38,7 +38,7 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
await this.processRunner.run(executablePath, { await this.processRunner.run(executablePath, {
templatePath: options.templatePath, templatePath: options.templatePath,
data: inputPath, data: inputPath,
workdir: outputPath, output: outputPath,
format: options.format, format: options.format,
}); });

View File

@ -31,6 +31,7 @@ export class RestDocumentSigningService implements IDocumentSigningService {
async sign(payload: Buffer, context: ICompanyCertificateContext): Promise<Buffer> { async sign(payload: Buffer, context: ICompanyCertificateContext): Promise<Buffer> {
if (!this.circuitBreaker.canExecute()) { if (!this.circuitBreaker.canExecute()) {
console.error(`Document signing service unavailable (${this.signUrl})`);
throw new Error("Document signing service unavailable (circuit open)"); throw new Error("Document signing service unavailable (circuit open)");
} }

View File

@ -169,11 +169,13 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
matches: (e) => isDocumentGenerationError(e), matches: (e) => isDocumentGenerationError(e),
build: (e) => { build: (e) => {
const error = e as DocumentGenerationError; const error = e as DocumentGenerationError;
const cause = error.cause as Error;
console.error(cause.message, cause);
const title = const title =
error.documentErrorType === "METADATA" error.documentErrorType === "METADATA"
? "Invalid document render error" ? "Invalid document render error"
: "Unexcepted document render error"; : "Unexcepted document render error";
return new InternalApiError(error.message, title); return new InternalApiError(cause.message, title);
}, },
}, },
{ {

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/customer-invoices", "name": "@erp/customer-invoices",
"version": "0.1.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -4,6 +4,7 @@ import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax
export interface IssuedInvoiceReportSnapshot { export interface IssuedInvoiceReportSnapshot {
id: string; id: string;
company_id: string; company_id: string;
company_slug: string;
invoice_number: string; invoice_number: string;
series: string; series: string;
status: string; status: string;

View File

@ -1,25 +0,0 @@
import type { IDocumentSigningService } from "../../services";
import {
type IIssuedInvoiceDocumentRenderer,
IssuedInvoiceDocumentReportService,
} from "../services";
export const buildIssuedInvoiceDocumentService = (
renderer: IIssuedInvoiceDocumentRenderer,
signingService: IDocumentSigningService,
certificateResolver: ICompanyCertificateResolver
) => {
const { fastReport } = renderers;
const issuedInvoiceReportRenderer = new IssuedInvoiceFastReportRenderer(
fastReport.executableResolver,
fastReport.processRunner,
fastReport.templateResolver,
fastReport.reportStorage
);
const issuedInvoiceDocumentRenderer = new IssuedInvoiceDocumentRenderer(
issuedInvoiceReportRenderer
);
return new IssuedInvoiceDocumentReportService(renderer, signingService, certificateResolver);
};

View File

@ -1,4 +1,3 @@
export * from "./documents.di";
export * from "./finder.di"; export * from "./finder.di";
export * from "./snapshot-builders.di"; export * from "./snapshot-builders.di";
export * from "./use-cases.di"; export * from "./use-cases.di";

View File

@ -25,6 +25,7 @@ export class IssuedInvoiceDocumentMetadataFactory
documentType: "issued-invoice", documentType: "issued-invoice",
documentId: snapshot.id, documentId: snapshot.id,
companyId: snapshot.company_id, companyId: snapshot.company_id,
companySlug: snapshot.company_slug,
format: "PDF", format: "PDF",
languageCode: snapshot.language_code ?? "es", languageCode: snapshot.language_code ?? "es",
filename: this.buildFilename(snapshot), filename: this.buildFilename(snapshot),

View File

@ -37,6 +37,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
return { return {
id: snapshot.id, id: snapshot.id,
company_id: snapshot.company_id, company_id: snapshot.company_id,
company_slug: "rodax",
invoice_number: snapshot.invoice_number, invoice_number: snapshot.invoice_number,
series: snapshot.series, series: snapshot.series,
status: snapshot.status, status: snapshot.status,

View File

@ -1,9 +1,9 @@
import type { import type {
ICertificateResolver,
IDocument, IDocument,
IDocumentMetadata, IDocumentMetadata,
IDocumentPostProcessor, IDocumentPostProcessor,
IDocumentSigningService, IDocumentSigningService,
ISigningContextResolver,
} from "@erp/core/api"; } from "@erp/core/api";
/** /**
@ -15,18 +15,21 @@ import type {
*/ */
export class DigitalSignaturePostProcessor implements IDocumentPostProcessor { export class DigitalSignaturePostProcessor implements IDocumentPostProcessor {
constructor( constructor(
private readonly certificateResolver: ICertificateResolver, private readonly certificateResolver: ISigningContextResolver,
private readonly signingService: IDocumentSigningService private readonly signingService: IDocumentSigningService
) {} ) {}
async process(document: IDocument, metadata: IDocumentMetadata): Promise<IDocument> { async process(document: IDocument, metadata: IDocumentMetadata): Promise<IDocument> {
// Validación defensiva mínima // Validación defensiva mínima
if (document.mimeType !== "application/pdf") { if (document.mimeType !== "application/pdf") {
throw new Error("DigitalSignaturePostProcessor can only sign PDF documents"); throw new Error("[DigitalSignaturePostProcessor] can only sign PDF documents");
} }
// 1. Resolver certificado de la empresa // 1. Resolver certificado de la empresa
const certificate = await this.certificateResolver.resolveForCompany(metadata.companyId); const certificate = await this.certificateResolver.resolveForCompany(metadata.companySlug);
if (!certificate) {
throw new Error("[DigitalSignaturePostProcessor] Compny certificate is undefined");
}
// 2. Firmar payload // 2. Firmar payload
const signedPayload = await this.signingService.sign(document.payload, certificate); const signedPayload = await this.signingService.sign(document.payload, certificate);

View File

@ -41,15 +41,15 @@ export class ReportIssuedInvoiceController extends ExpressController {
}); });
return result.match( return result.match(
({ data, filename }) => { ({ payload, filename }) => {
if (format === "PDF") { if (format === "PDF") {
return this.downloadPDF(data as Buffer<ArrayBuffer>, String(filename)); return this.downloadPDF(payload as Buffer<ArrayBuffer>, String(filename));
} }
if (format === "HTML") { if (format === "HTML") {
return this.downloadHTML(data as string); return this.downloadHTML(payload as unknown as string);
} }
// JSON // JSON
return this.json(data); return this.json(payload);
}, },
(err) => this.handleError(err) (err) => this.handleError(err)
); );

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/customers", "name": "@erp/customers",
"version": "0.1.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,7 +1,8 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerByIdResponseDTO } from "../../../../common/dto";
import { Customer } from "../../../domain"; import type { GetCustomerByIdResponseDTO } from "../../../../common/dto";
import type { Customer } from "../../../domain";
export class CustomerFullPresenter extends Presenter<Customer, GetCustomerByIdResponseDTO> { export class CustomerFullPresenter extends Presenter<Customer, GetCustomerByIdResponseDTO> {
toOutput(customer: Customer): GetCustomerByIdResponseDTO { toOutput(customer: Customer): GetCustomerByIdResponseDTO {

View File

@ -1,10 +1,11 @@
import { CriteriaDTO } from "@erp/core"; import type { CriteriaDTO } from "@erp/core";
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import type { Collection } from "@repo/rdx-utils";
import { ListCustomersResponseDTO } from "../../../../common/dto";
import { CustomerListDTO } from "../../../infrastructure/mappers"; import type { ListCustomersResponseDTO } from "../../../../common/dto";
import type { CustomerListDTO } from "../../../infrastructure/mappers";
export class ListCustomersPresenter extends Presenter { export class ListCustomersPresenter extends Presenter {
protected _mapCustomer(customer: CustomerListDTO) { protected _mapCustomer(customer: CustomerListDTO) {

View File

@ -4,15 +4,16 @@ import {
InMemoryPresenterRegistry, InMemoryPresenterRegistry,
SequelizeTransactionManager, SequelizeTransactionManager,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CreateCustomerUseCase, CreateCustomerUseCase,
CustomerApplicationService,
CustomerFullPresenter,
GetCustomerUseCase, GetCustomerUseCase,
ListCustomersPresenter,
ListCustomersUseCase, ListCustomersUseCase,
UpdateCustomerUseCase, UpdateCustomerUseCase,
} from "../application"; } from "../application";
import { CustomerApplicationService } from "../application/customer-application.service";
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
import { CustomerDomainMapper, CustomerListMapper } from "./mappers"; import { CustomerDomainMapper, CustomerListMapper } from "./mappers";
import { CustomerRepository } from "./sequelize"; import { CustomerRepository } from "./sequelize";

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/doc-numbering", "name": "@erp/doc-numbering",
"version": "0.1.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/rdx-criteria", "name": "@repo/rdx-criteria",
"version": "0.2.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/rdx-ddd", "name": "@repo/rdx-ddd",
"version": "0.2.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/rdx-logger", "name": "@repo/rdx-logger",
"version": "0.2.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/rdx-utils", "name": "@repo/rdx-utils",
"version": "0.2.7", "version": "0.3.6",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -66,7 +66,7 @@ services:
traefik.http.routers.factuges_rodax_phpmyadmin.middlewares: "factuges_rodax_phpmyadmin_strip" traefik.http.routers.factuges_rodax_phpmyadmin.middlewares: "factuges_rodax_phpmyadmin_strip"
# --- API (imagen versionada generada por build-api.sh) --- # --- API ---
api: api:
image: ${API_IMAGE} image: ${API_IMAGE}
container_name: factuges_rodax_api container_name: factuges_rodax_api
@ -76,18 +76,32 @@ services:
condition: service_healthy condition: service_healthy
environment: environment:
NODE_ENV: production NODE_ENV: production
COMPANY: rodax COMPANY_SLUG: ${COMPANY_SLUG}
PORT: ${SERVER_PORT:-3002} FRONTEND_URL: ${FRONTEND_URL}
DB_DIALECT: "mysql" API_PORT: ${API_PORT}
DB_HOST: "db" DB_HOST: "db"
DB_PORT: ${DB_PORT} DB_PORT: ${DB_PORT}
DB_DIALECT: ${DB_DIALECT}
DB_NAME: ${DB_NAME} DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER} DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS} DB_PASS: ${DB_PASS}
FRONTEND_URL: ${FRONTEND_URL}
TEMPLATES_PATH: ${TEMPLATES_PATH} TEMPLATES_PATH: ${TEMPLATES_PATH}
FASTREPORT_BIN: ${FASTREPORT_BIN}
DOCUMENTS_PATH: ${DOCUMENTS_PATH} DOCUMENTS_PATH: ${DOCUMENTS_PATH}
FASTREPORT_BIN: ${FASTREPORT_BIN}
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}
SIGNING_SERVICE_TIMEOUT_MS: ${SIGNING_SERVICE_TIMEOUT_MS}
SIGNING_SERVICE_MAX_RETRIES: ${SIGNING_SERVICE_MAX_RETRIES}
COMPANY_CERTIFICATES_JSON: ${COMPANY_CERTIFICATES_JSON}
volumes: volumes:
- ./volumes/templates:/shared/templates:ro - ./volumes/templates:/shared/templates:ro
- ./volumes/certificates:/shared/certificates:ro - ./volumes/certificates:/shared/certificates:ro

View File

@ -1,15 +1,18 @@
# Identidad de la compañía # Identidad de la compañía
COMPANY=rodax COMPANY_SLUG=rodax
# Dominios # Dominios
DOMAIN=factuges.rodax-software.local DOMAIN=factuges.rodax-software.local
# MariaDB # MariaDB
DB_ROOT_PASS=verysecret DB_HOST=db
DB_PORT=3306
DB_DIALECT=mysql
DB_NAME=rodax_db
DB_USER=rodax_usr DB_USER=rodax_usr
DB_PASS=supersecret DB_PASS=supersecret
DB_NAME=rodax_db DB_ROOT_PASS=verysecret
DB_PORT=3306
# API # API
API_PORT=3002 API_PORT=3002
@ -23,6 +26,22 @@ TEMPLATES_PATH=/shared/templates
FASTREPORT_BIN=/repo/tools/FastReportCliGenerator FASTREPORT_BIN=/repo/tools/FastReportCliGenerator
DOCUMENTS_PATH=/shared/documents 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_MAX_RETRIES=2
COMPANY_CERTIFICATES_JSON='{
"rodax": {
"certificateId": "no se que poner aqui",
"certificateSecretName": "certificate_secret_name",
"certificatePasswordSecretName": "certificate_password_secret_name"
}
}'
# SYNC # SYNC
ENV = development ENV = development