Nuevo sistema de carga de módulos + Generador de documetos
This commit is contained in:
parent
dc204237b1
commit
2b3dfce72c
@ -3,14 +3,22 @@ import type { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
import { logger } from "../logger";
|
||||
|
||||
import { initModels, registerModels } from "./model-loader";
|
||||
import { getService, listServices, registerService } from "./service-registry";
|
||||
import { getServiceScoped, listServices, registerService } from "./service-registry";
|
||||
|
||||
const registeredModules: Map<string, IModuleServer> = new Map();
|
||||
const initializedModules = new Set<string>();
|
||||
const visiting = new Set<string>(); // para detección de ciclos
|
||||
const initializationOrder: string[] = []; // orden de init → para warmup
|
||||
|
||||
// Config opcional para warmup (valores por defecto seguros)
|
||||
// Nuevo: control por fases
|
||||
const setupDone = new Set<string>(); // para detección de ciclos
|
||||
const visiting = new Set<string>();
|
||||
const setupOrder: string[] = [];
|
||||
|
||||
// Internal store
|
||||
const internalByModule: Map<string, Record<string, unknown>> = new Map();
|
||||
|
||||
// Tracking de dependencias realmente usadas por módulo
|
||||
const usedDependenciesByModule: Map<string, Set<string>> = new Map();
|
||||
|
||||
// Warmup config (valores por defecto seguros)
|
||||
const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000;
|
||||
const WARMUP_STRICT =
|
||||
String(process.env.WARMUP_STRICT ?? "false")
|
||||
@ -18,138 +26,236 @@ const WARMUP_STRICT =
|
||||
.trim() === "true";
|
||||
|
||||
/**
|
||||
Registra un módulo del servidor en el registry.
|
||||
Lanza error si el nombre ya existe.
|
||||
- Registra un módulo del servidor en el registry.
|
||||
- Lanza error si el nombre ya existe.
|
||||
*/
|
||||
export function registerModule(pkg: IModuleServer) {
|
||||
if (!pkg?.name) {
|
||||
throw new Error('❌ Invalid module: missing "name" property');
|
||||
}
|
||||
if (registeredModules.has(pkg.name)) {
|
||||
if (!pkg?.name) throw new Error('❌ Invalid module: missing "name" property');
|
||||
if (registeredModules.has(pkg.name))
|
||||
throw new Error(`❌ Module "${pkg.name}" already registered`);
|
||||
}
|
||||
registeredModules.set(pkg.name, pkg);
|
||||
logger.info(`📦 Module enqueued: "${pkg.name}"`, { label: "moduleRegistry" });
|
||||
}
|
||||
|
||||
/**
|
||||
Inicializa todos los módulos registrados (resolviendo dependencias),
|
||||
luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
|
||||
- Inicializa todos los módulos registrados (resolviendo dependencias),
|
||||
- luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
|
||||
*/
|
||||
export async function initModules(params: ModuleParams) {
|
||||
// 1) SETUP
|
||||
for (const name of registeredModules.keys()) {
|
||||
await loadModule(name, params, []); // secuencial para logs deterministas
|
||||
await setupModule(name, params, []); // secuencial para logs deterministas
|
||||
}
|
||||
|
||||
await withPhase("global", "initModels", async () => {
|
||||
await initModels(params);
|
||||
});
|
||||
// 2) MODELS global
|
||||
await withPhase("global", "initModels", async () => await initModels(params));
|
||||
|
||||
// 3) START por orden de setup
|
||||
for (const name of setupOrder) {
|
||||
const pkg = registeredModules.get(name);
|
||||
if (!pkg?.start) continue;
|
||||
|
||||
// Si aún hay rastros del anterior init/registerDependencies en algún módulo:
|
||||
// - start() ≈ init() en el contrato nuevo
|
||||
// - setup() ≈ registerDependencies() en el contrato nuevo
|
||||
await withPhase(name, "start", async () => {
|
||||
// Aquí YA se permite getService/getInternal
|
||||
await Promise.resolve(
|
||||
pkg.start?.({
|
||||
...params,
|
||||
getService: makeGetService(name, pkg),
|
||||
getInternal: makeGetInternal(name),
|
||||
listServices, // opcional: útil en debugging
|
||||
} as any)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 4) WARMUP opcional
|
||||
await warmupModules(params);
|
||||
|
||||
// 5) Validación dependencias usadas vs declaradas
|
||||
validateModuleDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
|
||||
*/
|
||||
async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
||||
if (initializedModules.has(name)) return;
|
||||
* SETUP recursivo: resuelve deps, ejecuta setup del módulo (antiguo "registerDependencies"),
|
||||
* registra modelos/servicios/internal, y guarda orden.
|
||||
*/
|
||||
async function setupModule(name: string, params: ModuleParams, stack: string[]) {
|
||||
if (setupDone.has(name)) return;
|
||||
|
||||
if (visiting.has(name)) {
|
||||
// Ciclo detectado: construir traza legible
|
||||
const cyclePath = [...stack, name].join(" -> ");
|
||||
throw new Error(`❌ Cyclic dependency detected: ${cyclePath}`);
|
||||
throw new Error(`❌ Cyclic dependency detected: ${[...stack, name].join(" -> ")}`);
|
||||
}
|
||||
|
||||
const pkg = registeredModules.get(name);
|
||||
if (!pkg) {
|
||||
if (!pkg)
|
||||
throw new Error(
|
||||
`❌ Module "${name}" not found (required by: ${stack[stack.length - 1] ?? "root"})`
|
||||
);
|
||||
}
|
||||
|
||||
visiting.add(name);
|
||||
stack.push(name);
|
||||
|
||||
// 1) Resolver dependencias primero (en orden)
|
||||
const deps = pkg.dependencies || [];
|
||||
for (const dep of deps) {
|
||||
await loadModule(dep, params, stack.slice());
|
||||
// 1) deps first
|
||||
for (const dep of pkg.dependencies ?? []) {
|
||||
await setupModule(dep, params, stack.slice());
|
||||
}
|
||||
|
||||
// 2) Inicializar el módulo (permite async)
|
||||
await withPhase(name, "init", async () => {
|
||||
await Promise.resolve(pkg.init(params));
|
||||
});
|
||||
|
||||
// 3) Registrar dependencias que expone (permite async)
|
||||
const pkgApi = await withPhase(name, "registerDependencies", async () => {
|
||||
// 2) setup phase (contrato nuevo) = registerDependencies (en tu código actual)
|
||||
const pkgApi = await withPhase(name, "setup", async () => {
|
||||
return await Promise.resolve(
|
||||
pkg.registerDependencies?.({
|
||||
pkg.setup?.({
|
||||
...params,
|
||||
// En setup se permite getService para wiring.
|
||||
getService: makeGetService(name, pkg),
|
||||
listServices,
|
||||
getService,
|
||||
})
|
||||
} as any)
|
||||
);
|
||||
});
|
||||
|
||||
// 4) Registrar modelos de Sequelize, si existen
|
||||
// 3) internal store
|
||||
if (pkgApi?.internal) {
|
||||
internalByModule.set(name, pkgApi.internal as Record<string, unknown>);
|
||||
} else {
|
||||
internalByModule.set(name, {});
|
||||
}
|
||||
|
||||
// 4) models
|
||||
if (pkgApi?.models) {
|
||||
await withPhase(name, "registerModels", async () => {
|
||||
await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: pkg.name }));
|
||||
await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: name }));
|
||||
});
|
||||
}
|
||||
|
||||
// 5) Registrar servicios, si existen
|
||||
if (pkgApi?.services && typeof pkgApi.services === "object") {
|
||||
// 5) services (namespaced)
|
||||
if (pkgApi?.services) {
|
||||
await withPhase(name, "registerServices", async () => {
|
||||
await Promise.resolve(registerService(pkg.name, pkgApi.services));
|
||||
for (const [serviceKey, serviceApi] of Object.entries(pkgApi.services!)) {
|
||||
registerService(`${name}:${serviceKey}`, serviceApi);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializedModules.add(name);
|
||||
initializationOrder.push(name); // recordamos el orden para warmup
|
||||
setupDone.add(name);
|
||||
setupOrder.push(name);
|
||||
|
||||
visiting.delete(name);
|
||||
stack.pop();
|
||||
|
||||
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
|
||||
logger.info(`✅ Module "${name}" setup done`, { label: "moduleRegistry" });
|
||||
}
|
||||
|
||||
/** getService instrumentado + scoped */
|
||||
function makeGetService(moduleName: string, pkg: IModuleServer) {
|
||||
return <T>(serviceName: string): T => {
|
||||
const [serviceModule] = serviceName.split(":");
|
||||
trackDependencyUse(moduleName, serviceModule);
|
||||
|
||||
// IMPORTANTE: devolver el valor
|
||||
return getServiceScoped<T>(moduleName, pkg.dependencies ?? [], serviceName);
|
||||
};
|
||||
}
|
||||
|
||||
/** getInternal: por defecto solo permite al propio módulo acceder a su internal */
|
||||
function makeGetInternal(requesterModule: string) {
|
||||
return <T>(moduleName: string, key?: string): T => {
|
||||
if (moduleName !== requesterModule) {
|
||||
throw new Error(
|
||||
`Module "${requesterModule}" attempted to access internal of "${moduleName}". ` +
|
||||
"Internal is private to the owning module."
|
||||
);
|
||||
}
|
||||
|
||||
const internal = internalByModule.get(moduleName) ?? {};
|
||||
if (!key) return internal as unknown as T;
|
||||
|
||||
if (!(key in internal)) {
|
||||
throw new Error(`Internal key "${key}" not found in module "${moduleName}".`);
|
||||
}
|
||||
return internal[key] as T;
|
||||
};
|
||||
}
|
||||
|
||||
function trackDependencyUse(requester: string, dep: string) {
|
||||
let set = usedDependenciesByModule.get(requester);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
usedDependenciesByModule.set(requester, set);
|
||||
}
|
||||
set.add(dep);
|
||||
}
|
||||
|
||||
function validateModuleDependencies() {
|
||||
for (const [moduleName, pkg] of registeredModules.entries()) {
|
||||
const declared = new Set(pkg.dependencies ?? []);
|
||||
const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>();
|
||||
|
||||
// ❌ usadas pero no declaradas
|
||||
const undeclaredUsed = [...used].filter((d) => !declared.has(d));
|
||||
|
||||
if (undeclaredUsed.length > 0) {
|
||||
throw new Error(
|
||||
`❌ Module "${moduleName}" used undeclared dependencies: ${undeclaredUsed.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
// ⚠️ declaradas pero no usadas (opcional)
|
||||
const unusedDeclared = [...declared].filter((d) => !used.has(d));
|
||||
if (unusedDeclared.length > 0) {
|
||||
logger.warn(
|
||||
`⚠️ Module "${moduleName}" declared unused dependencies: ${unusedDeclared.join(", ")}`,
|
||||
{ label: "moduleRegistry", module: moduleName }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Ejecuta warmup() opcional de cada módulo en orden de inicialización.
|
||||
Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
|
||||
- Ejecuta warmup() opcional de cada módulo en orden de inicialización.
|
||||
- Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
|
||||
*/
|
||||
async function warmupModules(params: ModuleParams) {
|
||||
logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" });
|
||||
|
||||
for (const name of initializationOrder) {
|
||||
for (const name of setupOrder) {
|
||||
const pkg = registeredModules.get(name);
|
||||
if (!pkg) continue;
|
||||
if (!pkg?.warmup) continue;
|
||||
|
||||
const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise<void> | void })
|
||||
.warmup;
|
||||
const maybeWarmup = (pkg as any).warmup as undefined | ((p: any) => Promise<void> | void);
|
||||
if (typeof maybeWarmup !== "function") continue;
|
||||
|
||||
if (typeof maybeWarmup === "function") {
|
||||
try {
|
||||
await withPhase(name, "warmup", () =>
|
||||
withTimeout(Promise.resolve(maybeWarmup(params)), WARMUP_TIMEOUT_MS, `${name}.warmup`)
|
||||
);
|
||||
} catch (error) {
|
||||
if (WARMUP_STRICT) {
|
||||
logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
|
||||
label: "moduleRegistry",
|
||||
module: name,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
|
||||
try {
|
||||
await withPhase(name, "warmup", () =>
|
||||
withTimeout(
|
||||
Promise.resolve(
|
||||
maybeWarmup({
|
||||
...params,
|
||||
getService: makeGetService(name, pkg),
|
||||
getInternal: makeGetInternal(name),
|
||||
listServices,
|
||||
})
|
||||
),
|
||||
WARMUP_TIMEOUT_MS,
|
||||
`${name}.warmup`
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (WARMUP_STRICT) {
|
||||
logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
|
||||
label: "moduleRegistry",
|
||||
module: name,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
|
||||
label: "moduleRegistry",
|
||||
module: name,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,17 +263,11 @@ async function warmupModules(params: ModuleParams) {
|
||||
}
|
||||
|
||||
/**
|
||||
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
|
||||
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
|
||||
*/
|
||||
async function withPhase<T>(
|
||||
moduleName: string,
|
||||
phase:
|
||||
| "init"
|
||||
| "registerDependencies"
|
||||
| "registerModels"
|
||||
| "registerServices"
|
||||
| "initModels"
|
||||
| "warmup",
|
||||
phase: "setup" | "start" | "registerModels" | "registerServices" | "initModels" | "warmup",
|
||||
fn: () => Promise<T> | T
|
||||
): Promise<T> {
|
||||
const startedAt = Date.now();
|
||||
|
||||
@ -10,10 +10,32 @@ export function registerService(name: string, api: any) {
|
||||
services[name] = api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera un servicio registrado bajo un "scope".
|
||||
* getService("customers:repository")
|
||||
* Debe declarar: dependencies: ["customers"]
|
||||
*/
|
||||
export function getServiceScoped<T = any>(
|
||||
requesterModule: string,
|
||||
allowedDeps: readonly string[],
|
||||
name: string
|
||||
): T {
|
||||
const [serviceModule] = name.split(":");
|
||||
|
||||
if (!allowedDeps.includes(serviceModule)) {
|
||||
throw new Error(
|
||||
`❌ Module "${requesterModule}" tried to access service "${name}" ` +
|
||||
`without declaring dependency on "${serviceModule}"`
|
||||
);
|
||||
}
|
||||
|
||||
return getService<T>(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera un servicio registrado, con tipado opcional.
|
||||
*/
|
||||
export function getService<T = any>(name: string): T {
|
||||
function getService<T = any>(name: string): T {
|
||||
const service = services[name];
|
||||
if (!service) {
|
||||
throw new Error(`❌ Servicio "${name}" no encontrado.`);
|
||||
|
||||
@ -172,7 +172,7 @@
|
||||
"useYield": "error"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "error",
|
||||
"noBannedTypes": "off",
|
||||
"noExcessiveCognitiveComplexity": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
|
||||
1
input.json
Normal file
1
input.json
Normal file
File diff suppressed because one or more lines are too long
@ -20,6 +20,9 @@ import type { IDocumentSideEffect } from "./document-side-effect.interface";
|
||||
* 4. Post-processors (firma, watermark, etc.)
|
||||
* 5. Side-effects (persistencia, métricas) [best-effort]
|
||||
*/
|
||||
|
||||
export type DocumentGenerationServiceRenderParams = Record<string, string>;
|
||||
|
||||
export class DocumentGenerationService<TSnapshot> {
|
||||
constructor(
|
||||
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>,
|
||||
@ -29,7 +32,10 @@ export class DocumentGenerationService<TSnapshot> {
|
||||
private readonly sideEffects: readonly IDocumentSideEffect[]
|
||||
) {}
|
||||
|
||||
async generate(snapshot: TSnapshot): Promise<Result<IDocument, DocumentGenerationError>> {
|
||||
async generate(
|
||||
snapshot: TSnapshot,
|
||||
params: DocumentGenerationServiceRenderParams
|
||||
): Promise<Result<IDocument, DocumentGenerationError>> {
|
||||
let metadata: IDocumentMetadata;
|
||||
|
||||
// 1. Metadata
|
||||
@ -55,6 +61,7 @@ export class DocumentGenerationService<TSnapshot> {
|
||||
let document: IDocument;
|
||||
try {
|
||||
document = await this.renderer.render(snapshot, {
|
||||
...params,
|
||||
format: metadata.format,
|
||||
languageCode: metadata.languageCode,
|
||||
filename: metadata.filename,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IDocument, IDocumentMetadata } from "../application-models";
|
||||
import type { IDocument } from "../application-models";
|
||||
|
||||
export interface IDocumentStorage {
|
||||
/**
|
||||
@ -8,5 +8,5 @@ export interface IDocumentStorage {
|
||||
* - Best-effort
|
||||
* - Nunca lanza (errores se gestionan internamente)
|
||||
*/
|
||||
save(document: IDocument, metadata: IDocumentMetadata): Promise<void>;
|
||||
save(document: IDocument, metadata: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
@ -2,4 +2,3 @@ export * from "./renderer";
|
||||
export * from "./renderer.interface";
|
||||
export * from "./renderer-registry";
|
||||
export * from "./renderer-registry.interface";
|
||||
export * from "./renderer-template-resolver.interface";
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export interface IRendererTemplateResolver {
|
||||
/** Devuelve la ruta absoluta del fichero de plantilla */
|
||||
resolveTemplatePath(module: string, companySlug: string, templateName: string): string;
|
||||
}
|
||||
@ -3,16 +3,20 @@ import {
|
||||
FastReportExecutableResolver,
|
||||
FastReportProcessRunner,
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
FilesystemDocumentCacheStore,
|
||||
RestDocumentSigningService,
|
||||
} from "../documents";
|
||||
import { FilesystemDocumentStorage } from "../storage";
|
||||
|
||||
export function buildCoreDocumentsDI(env: NodeJS.ProcessEnv) {
|
||||
export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
|
||||
const { TEMPLATES_PATH } = env;
|
||||
|
||||
// Renderers
|
||||
const frExecutableResolver = new FastReportExecutableResolver(env.FASTREPORT_BIN);
|
||||
const frProcessRunner = new FastReportProcessRunner();
|
||||
const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner);
|
||||
const fastReportTemplateResolver = new FastReportTemplateResolver(TEMPLATES_PATH!);
|
||||
|
||||
// Signing
|
||||
const signingContextResolver = new EnvCompanySigningContextResolver(env);
|
||||
@ -32,6 +36,7 @@ export function buildCoreDocumentsDI(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
documentRenderers: {
|
||||
fastReportRenderer,
|
||||
fastReportTemplateResolver,
|
||||
},
|
||||
documentSigning: {
|
||||
signingService,
|
||||
@ -42,4 +47,4 @@ export function buildCoreDocumentsDI(env: NodeJS.ProcessEnv) {
|
||||
storage,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { FastReportTemplateNotFoundError } from "./fastreport-errors";
|
||||
|
||||
/**
|
||||
* Resuelve rutas de plantillas para desarrollo y producción.
|
||||
*/
|
||||
export class FastReportTemplateResolver {
|
||||
constructor(protected readonly rootPath: string) {}
|
||||
|
||||
/** Une partes de ruta relativas al rootPath */
|
||||
protected resolveJoin(parts: string[]): string {
|
||||
return join(this.rootPath, ...parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
|
||||
* según el entorno (dev/prod).
|
||||
*/
|
||||
protected resolveTemplateDirectory(module: string, companySlug: string): string {
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
if (isDev) {
|
||||
// <root>/<module>/templates/<companySlug>/
|
||||
return this.resolveJoin([module, "templates", companySlug]);
|
||||
}
|
||||
|
||||
// <root>/templates/<module>/<companySlug>/
|
||||
//return this.resolveJoin(["templates", module, companySlug]);
|
||||
return this.resolveJoin([module]);
|
||||
}
|
||||
|
||||
/** Resuelve una ruta de recurso relativa al directorio de plantilla */
|
||||
protected resolveAssetPath(templateDir: string, relative: string): string {
|
||||
return join(templateDir, relative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la ruta absoluta del fichero de plantilla.
|
||||
*/
|
||||
public resolveTemplatePath(module: string, companySlug: string, templateName: string): string {
|
||||
const dir = this.resolveTemplateDirectory(module, companySlug);
|
||||
const filePath = this.resolveAssetPath(dir, templateName);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new FastReportTemplateNotFoundError(
|
||||
`Template not found: module=${module} company=${companySlug} name=${templateName}`
|
||||
);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/** Lee el contenido de un fichero plantilla */
|
||||
protected readTemplateFile(templatePath: string): string {
|
||||
return readFileSync(templatePath, "utf8");
|
||||
}
|
||||
}
|
||||
@ -3,3 +3,4 @@ export * from "./fastreport-executable-resolver";
|
||||
export * from "./fastreport-process-runner";
|
||||
export * from "./fastreport-render-options.type";
|
||||
export * from "./fastreport-renderer";
|
||||
export * from "./fastreport-template-resolver";
|
||||
|
||||
@ -1,9 +1,34 @@
|
||||
//Contrato para los Modules backend (Node.js)
|
||||
|
||||
import { ModuleMetadata } from "../../common";
|
||||
import { ModuleDependencies, ModuleParams } from "./types";
|
||||
import type { ModuleMetadata } from "../../common";
|
||||
|
||||
import type { ModuleSetupResult, SetupParams, StartParams, WarmupParams } from "./types";
|
||||
|
||||
export interface IModuleServer extends ModuleMetadata {
|
||||
init(params: ModuleParams): Promise<void>;
|
||||
registerDependencies?(params: ModuleParams): Promise<ModuleDependencies>;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Módulos de los que depende este módulo.
|
||||
* Deben existir y cargarse antes.
|
||||
*/
|
||||
dependencies?: string[];
|
||||
|
||||
/**
|
||||
* Fase de construcción del módulo.
|
||||
* Aquí se construye el dominio y se decide qué se expone.
|
||||
*/
|
||||
setup?: (params: SetupParams) => Promise<ModuleSetupResult> | ModuleSetupResult;
|
||||
|
||||
/**
|
||||
* Fase de arranque del módulo.
|
||||
* Aquí se conecta el módulo al runtime.
|
||||
*/
|
||||
start?: (params: StartParams) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Warmup opcional (IO, precargas, validaciones externas).
|
||||
*/
|
||||
warmup?: (params: WarmupParams) => Promise<void> | void;
|
||||
}
|
||||
|
||||
@ -15,3 +15,29 @@ export interface ModuleDependencies {
|
||||
models?: any[];
|
||||
services?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export type ModuleSetupResult = {
|
||||
models?: any[];
|
||||
services?: Record<string, unknown>;
|
||||
internal?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SetupParams = ModuleParams & {
|
||||
registerService: (name: string, api: unknown) => void;
|
||||
};
|
||||
|
||||
export type StartParams = ModuleParams & {
|
||||
/**
|
||||
* Acceso a servicios expuestos por otros módulos.
|
||||
* Todas las dependencias ya están resueltas.
|
||||
*/
|
||||
getService: <T>(serviceName: string) => T;
|
||||
|
||||
/**
|
||||
* Acceso a internal del propio módulo.
|
||||
* No debe usarse para consumir otros módulos.
|
||||
*/
|
||||
getInternal: <T>(moduleName: string, key?: string) => T;
|
||||
};
|
||||
|
||||
export type WarmupParams = StartParams;
|
||||
|
||||
@ -7,12 +7,14 @@ export interface IssuedInvoiceReportSnapshot {
|
||||
invoice_number: string;
|
||||
series: string;
|
||||
status: string;
|
||||
reference: string;
|
||||
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
invoice_date: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
|
||||
recipient: {
|
||||
name: string;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { IIssuedInvoiceDocumentReportService, IIssuedInvoiceFinder } from "../services";
|
||||
import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../services";
|
||||
import type { IIssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders";
|
||||
import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders/full";
|
||||
import type { IIssuedInvoiceReportSnapshotBuilder } from "../snapshot-builders/report";
|
||||
@ -38,7 +38,7 @@ export function buildReportIssuedInvoiceUseCase(deps: {
|
||||
finder: IIssuedInvoiceFinder;
|
||||
fullSnapshotBuilder: IIssuedInvoiceFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: IIssuedInvoiceReportSnapshotBuilder;
|
||||
documentService: IIssuedInvoiceDocumentReportService;
|
||||
documentService: IssuedInvoiceDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ReportIssuedInvoiceUseCase(
|
||||
|
||||
@ -2,4 +2,3 @@ export * from "./issued-invoice-document-generator.interface";
|
||||
export * from "./issued-invoice-document-metadata-factory";
|
||||
export * from "./issued-invoice-document-renderer.interface";
|
||||
export * from "./issued-invoice-finder";
|
||||
export * from "./issued-invoice-report-snapshot-builder";
|
||||
|
||||
@ -14,7 +14,7 @@ export class IssuedInvoiceDocumentMetadataFactory
|
||||
{
|
||||
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentMetadata {
|
||||
if (!snapshot.id) {
|
||||
throw new Error("IssuedInvoiceReportSnapshot.invoiceId is required");
|
||||
throw new Error("IssuedInvoiceReportSnapshot.id is required");
|
||||
}
|
||||
|
||||
if (!snapshot.company_id) {
|
||||
|
||||
@ -40,6 +40,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
|
||||
invoice_number: snapshot.invoice_number,
|
||||
series: snapshot.series,
|
||||
status: snapshot.status,
|
||||
reference: snapshot.reference,
|
||||
|
||||
language_code: snapshot.language_code,
|
||||
currency_code: snapshot.currency_code,
|
||||
@ -47,6 +48,7 @@ export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportS
|
||||
invoice_date: DateHelper.format(snapshot.invoice_date, locale),
|
||||
|
||||
payment_method: snapshot.payment_method?.payment_description ?? "",
|
||||
notes: snapshot.notes,
|
||||
|
||||
recipient: {
|
||||
name: snapshot.recipient.name,
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import type { DTO } from "@erp/core";
|
||||
import type { ITransactionManager, RendererFormat } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CustomerInvoice } from "../../../../domain";
|
||||
import type { IssuedInvoiceDocumentGeneratorService } from "../../../../infrastructure/documents";
|
||||
import type { IIssuedInvoiceFinder } from "../../services";
|
||||
import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../../services";
|
||||
import type { IIssuedInvoiceFullSnapshotBuilder } from "../../snapshot-builders";
|
||||
import type { IIssuedInvoiceReportSnapshotBuilder } from "../../snapshot-builders/report";
|
||||
|
||||
@ -59,10 +56,8 @@ export class ReportIssuedInvoiceUseCase {
|
||||
});
|
||||
|
||||
// Llamar al servicio y que se apañe
|
||||
const documentResult = await this.documentGenerationService.generate({
|
||||
companyId,
|
||||
invoiceId,
|
||||
snapshot: reportSnapshot,
|
||||
const documentResult = await this.documentGenerationService.generate(reportSnapshot, {
|
||||
companySlug: "rodax",
|
||||
});
|
||||
|
||||
if (documentResult.isFailure) {
|
||||
@ -79,92 +74,5 @@ export class ReportIssuedInvoiceUseCase {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar factura emitida en transacción corta (solo lectura)
|
||||
const invoiceOrError = await this.transactionManager.complete(async (transaction) => {
|
||||
return this.invoiceRepository.findIssuedInvoiceByIdInCompany(
|
||||
input.companyId,
|
||||
invoiceId,
|
||||
transaction
|
||||
);
|
||||
});
|
||||
|
||||
if (invoiceOrError.isFailure) {
|
||||
return Result.fail(invoiceOrError.error);
|
||||
}
|
||||
|
||||
const issuedInvoice = invoiceOrError.data;
|
||||
|
||||
// Delegar completamente la generación del documento legal (snapshot → render → firma → cache)
|
||||
try {
|
||||
const document = await this.issuedInvoiceDocumentService.generate(issuedInvoice);
|
||||
|
||||
return Result.ok(document);
|
||||
} catch (error) {
|
||||
// Errores no esperados (infra / firma / filesystem)
|
||||
// Se dejan subir como fallo del caso de uso
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
const fullPresenter = this.presenterRegistry.getPresenter<CustomerInvoice>({
|
||||
resource: "issued-invoice",
|
||||
projection: "FULL",
|
||||
});
|
||||
|
||||
const reportPresenter = this.presenterRegistry.getPresenter<DTO>({
|
||||
resource: "issued-invoice",
|
||||
projection: "REPORT",
|
||||
});
|
||||
|
||||
const renderer = this.rendererRegistry.getRenderer<DTO>({
|
||||
resource: "issued-invoice",
|
||||
format,
|
||||
});
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany(
|
||||
companyId,
|
||||
invoiceId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (invoiceOrError.isFailure) {
|
||||
return Result.fail(invoiceOrError.error);
|
||||
}
|
||||
|
||||
const invoice = invoiceOrError.data;
|
||||
const documentId = `${invoice.series.getOrUndefined()}${invoice.invoiceNumber.toString()}`;
|
||||
|
||||
const invoiceDTO = await fullPresenter.toOutput(invoice, { companySlug });
|
||||
|
||||
const reportInvoiceDTO = await reportPresenter.toOutput(invoiceDTO, { companySlug });
|
||||
|
||||
const result = (await renderer.render(reportInvoiceDTO, {
|
||||
companySlug,
|
||||
documentId,
|
||||
})) as unknown;
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const { payload: invoiceRendered } = result.data;
|
||||
|
||||
if (format === "HTML") {
|
||||
return Result.ok({
|
||||
data: String(invoiceRendered),
|
||||
filename: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
data: invoiceRendered as Buffer<ArrayBuffer>,
|
||||
filename: `customer-invoice-${invoice.invoiceNumber}.pdf`,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { buildCoreDocumentsDI } from "@erp/core/api";
|
||||
|
||||
import { httpClient } from "./http-client";
|
||||
import { buildIssuedInvoicesDependencies } from "./infrastructure";
|
||||
import { buildTransactionManager } from "./infrastructure/di/repositories.di";
|
||||
|
||||
const coreInfra = buildCoreDocumentsDI({
|
||||
fastReport: {
|
||||
templatesBasePath: "/var/app/templates",
|
||||
},
|
||||
signing: {
|
||||
endpoint: "http://localhost:8000",
|
||||
httpClient,
|
||||
},
|
||||
storage: {
|
||||
basePath: "/var/app/reports",
|
||||
},
|
||||
});
|
||||
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
|
||||
const issuedInvoicesModule = buildIssuedInvoicesDependencies({
|
||||
fastReport: coreInfra.fastReport,
|
||||
documentSigningService: coreInfra.documentSigningService,
|
||||
certificateResolver: coreInfra.certificateResolver,
|
||||
transactionManager,
|
||||
});
|
||||
|
||||
export const reportIssuedInvoiceUseCase = issuedInvoicesModule.reportIssuedInvoiceUseCase;
|
||||
@ -1,68 +1,89 @@
|
||||
import type { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import { models } from "./infrastructure";
|
||||
import { issuedInvoicesRouter } from "./infrastructure/express/issued-invoices/issued-invoices.routes";
|
||||
import {
|
||||
type IssuedInvoicesInternalDeps,
|
||||
buildIssuedInvoicesDependencies,
|
||||
buildIssuedInvoicesServices,
|
||||
issuedInvoicesRouter,
|
||||
models,
|
||||
} from "./infrastructure";
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
version: "1.0.0",
|
||||
dependencies: ["customers"],
|
||||
|
||||
async init(params: ModuleParams) {
|
||||
const { logger, env } = params;
|
||||
/**
|
||||
* Fase de SETUP
|
||||
* ----------------
|
||||
* - Construye el dominio (una sola vez)
|
||||
* - Define qué expone el módulo
|
||||
* - NO conecta infraestructura
|
||||
*/
|
||||
async setup(params) {
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
//proformasRouter(params);
|
||||
// 1) Dominio interno
|
||||
const issuedInvoicesInternalDeps = buildIssuedInvoicesDependencies(params);
|
||||
//const proformasInternalDeps = buildProformasDependencies(params);
|
||||
|
||||
issuedInvoicesRouter({
|
||||
...params,
|
||||
services: issuedInvoicesServices,
|
||||
});
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const issuedInvoicesServices = buildIssuedInvoicesServices(issuedInvoicesInternalDeps);
|
||||
//const proformasServices = buildProformasServices(proformasInternalDeps);
|
||||
|
||||
logger.info("🚀 CustomerInvoices module initialized", { label: this.name });
|
||||
},
|
||||
|
||||
async registerDependencies(
|
||||
params: ModuleParams & {
|
||||
getService: (name: string) => any;
|
||||
}
|
||||
) {
|
||||
const { logger, getService } = params;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 1️⃣ Obtener dependencias CORE
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const coreDocuments = getService("core.documents");
|
||||
const transactionManager = getService("core.transactionManager");
|
||||
|
||||
if (!coreDocuments) {
|
||||
throw new Error("core.documents service not available");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 2️⃣ Construir DI del módulo
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
issuedInvoicesServices = buildIssuedInvoicesDI({
|
||||
coreDocuments,
|
||||
transactionManager,
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 3️⃣ Exponer servicios del módulo
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
logger.info("🚀 CustomerInvoices module dependencies registered", {
|
||||
label: this.name,
|
||||
});
|
||||
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
|
||||
|
||||
return {
|
||||
// Modelos Sequelize del módulo
|
||||
models,
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
issuedInvoices: issuedInvoicesServices,
|
||||
//proformas: proformasServices
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
issuedInvoices: issuedInvoicesInternalDeps,
|
||||
//proformas: proformasInternalDeps
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Fase de START
|
||||
* -------------
|
||||
* - Conecta el módulo al runtime
|
||||
* - Puede usar servicios e internals ya construidos
|
||||
* - NO construye dominio
|
||||
*/
|
||||
async start(params) {
|
||||
const { app, baseRoutePath, logger, getInternal } = params;
|
||||
|
||||
// Recuperamos el dominio interno del módulo
|
||||
const issuedInvoicesInternalDeps = getInternal<IssuedInvoicesInternalDeps>(
|
||||
"customer-invoices",
|
||||
"issuedInvoices"
|
||||
);
|
||||
//const proformasInternalDeps = getInternal("customer-invoices", "proformas");
|
||||
|
||||
// Registro de rutas HTTP
|
||||
issuedInvoicesRouter(params, issuedInvoicesInternalDeps);
|
||||
//proformasRouter(params, proformasInternalDeps);
|
||||
|
||||
logger.info("🚀 CustomerInvoices module started", {
|
||||
label: this.name,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Warmup opcional (si lo necesitas en el futuro)
|
||||
* ----------------------------------------------
|
||||
* warmup(params) {
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
};
|
||||
|
||||
export default customerInvoicesAPIModule;
|
||||
|
||||
@ -1,30 +1,12 @@
|
||||
import type {
|
||||
FastReportRenderer,
|
||||
IDocumentCacheStore,
|
||||
IDocumentSigningService,
|
||||
IDocumentStorage,
|
||||
ISigningContextResolver,
|
||||
} from "@erp/core/api";
|
||||
import { buildCoreDocumentsDI } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
IssuedInvoiceDocumentPipelineFactory,
|
||||
type IssuedInvoiceDocumentPipelineFactoryDeps,
|
||||
} from "../documents";
|
||||
|
||||
export const buildIssuedInvoiceDocumentService = (deps: {
|
||||
documentRenderers: {
|
||||
fastReportRenderer: FastReportRenderer;
|
||||
};
|
||||
documentSigning: {
|
||||
signingService: IDocumentSigningService;
|
||||
signingContextResolver: ISigningContextResolver;
|
||||
};
|
||||
documentStorage: {
|
||||
cacheStore: IDocumentCacheStore;
|
||||
storage: IDocumentStorage;
|
||||
};
|
||||
}) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = deps;
|
||||
export const buildIssuedInvoiceDocumentService = (env: NodeJS.ProcessEnv) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env);
|
||||
|
||||
const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = {
|
||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||
@ -36,6 +18,8 @@ export const buildIssuedInvoiceDocumentService = (deps: {
|
||||
//
|
||||
documentCacheStore: documentStorage.cacheStore,
|
||||
documentStorage: documentStorage.storage,
|
||||
|
||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
||||
};
|
||||
|
||||
const documentGeneratorPipeline = IssuedInvoiceDocumentPipelineFactory.create(pipelineDeps);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issued-invoices.di";
|
||||
//export * from "./proformas.di";
|
||||
export * from "./issued-invoices-services";
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
||||
|
||||
export type IssuedInvoicesServiceslDeps = {
|
||||
services: {
|
||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildIssuedInvoicesServices(
|
||||
deps: IssuedInvoicesInternalDeps
|
||||
): IssuedInvoicesServiceslDeps {
|
||||
return {
|
||||
services: {
|
||||
listIssuedInvoices: (filters, context) => null,
|
||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||
|
||||
getIssuedInvoiceById: (id, context) => null,
|
||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||
|
||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,12 +1,6 @@
|
||||
// modules/invoice/infrastructure/invoice-dependencies.factory.ts
|
||||
|
||||
import {
|
||||
type IDocumentSigningService,
|
||||
type ITransactionManager,
|
||||
type ModuleParams,
|
||||
buildCoreDocumentsDI,
|
||||
buildTransactionManager,
|
||||
} from "@erp/core/api";
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type GetIssuedInvoiceByIdUseCase,
|
||||
@ -18,114 +12,53 @@ import {
|
||||
buildListIssuedInvoicesUseCase,
|
||||
buildReportIssuedInvoiceUseCase,
|
||||
} from "../../application/issued-invoices";
|
||||
import { IssuedInvoiceDocumentPipelineFactory } from "../documents";
|
||||
|
||||
import { buildIssuedInvoiceDocumentService } from "./documents.di";
|
||||
import { buildRepository } from "./repositories.di";
|
||||
|
||||
export type IssuedInvoicesDeps = {
|
||||
export type IssuedInvoicesInternalDeps = {
|
||||
useCases: {
|
||||
list_issued_invoices: () => ListIssuedInvoicesUseCase;
|
||||
get_issued_invoice_by_id: () => GetIssuedInvoiceByIdUseCase;
|
||||
report_issued_invoice: () => ReportIssuedInvoiceUseCase;
|
||||
listIssuedInvoices: () => ListIssuedInvoicesUseCase;
|
||||
getIssuedInvoiceById: () => GetIssuedInvoiceByIdUseCase;
|
||||
reportIssuedInvoice: () => ReportIssuedInvoiceUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildIssuedInvoicesDI(params: ModuleParams) {
|
||||
export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesInternalDeps {
|
||||
const { database, env } = params;
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env);
|
||||
|
||||
// Infrastructure
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const repository = buildRepository(database);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 4️⃣ Finder + snapshot builders (APPLICATION)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// Application helpers
|
||||
const finder = buildIssuedInvoiceFinder(repository);
|
||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(env);
|
||||
|
||||
const issuedInvoiceDocumentPipeline = new IssuedInvoiceDocumentPipelineFactory({
|
||||
renderer: documentRenderers.fastReportRenderer,
|
||||
}).create;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 5️⃣ Use Case
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// Internal use cases (factories)
|
||||
return {
|
||||
useCases: {
|
||||
list_issued_invoices: () =>
|
||||
listIssuedInvoices: () =>
|
||||
buildListIssuedInvoicesUseCase({
|
||||
finder,
|
||||
itemSnapshotBuilder: snapshotBuilders.list,
|
||||
transactionManager,
|
||||
}),
|
||||
get_issued_invoice_by_id: () =>
|
||||
|
||||
getIssuedInvoiceById: () =>
|
||||
buildGetIssuedInvoiceByIdUseCase({
|
||||
finder,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
report_issued_invoice: () =>
|
||||
|
||||
reportIssuedInvoice: () =>
|
||||
buildReportIssuedInvoiceUseCase({
|
||||
finder,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
reportSnapshotBuilder: snapshotBuilders.report,
|
||||
documentService: issuedInvoiceDocumentService,
|
||||
transactionManager,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
reportIssuedInvoiceUseCase,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildIssuedInvoicesDependencies(deps: {
|
||||
documentSigningService: IDocumentSigningService;
|
||||
certificateResolver: ICertificateResolver;
|
||||
|
||||
transactionManager: ITransactionManager;
|
||||
}): IssuedInvoicesDeps {
|
||||
const { database, env } = params;
|
||||
const templateRootPath = env.TEMPLATES_PATH;
|
||||
const documentRootPath = env.DOCUMENTS_PATH;
|
||||
|
||||
/** Infraestructura */
|
||||
const repository = buildRepository(database);
|
||||
|
||||
//
|
||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||
const finder = buildIssuedInvoiceFinder(repository);
|
||||
const renderers = buildIssuedInvoiceReportRenderers(templateRootPath, documentRootPath);
|
||||
|
||||
const reportService = buildIssuedInvoiceReportService(
|
||||
renderers.fastReportPDFRenderer,
|
||||
signingService,
|
||||
certificateResolver
|
||||
);
|
||||
|
||||
return {
|
||||
useCases: {
|
||||
list_issued_invoices: () =>
|
||||
buildListIssuedInvoicesUseCase({
|
||||
finder,
|
||||
itemSnapshotBuilder: snapshotBuilders.list,
|
||||
transactionManager,
|
||||
}),
|
||||
get_issued_invoice_by_id: () =>
|
||||
buildGetIssuedInvoiceByIdUseCase({
|
||||
finder,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
report_issued_invoice: () =>
|
||||
buildReportIssuedInvoiceUseCase({
|
||||
finder,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
reportSnapshotBuilder: snapshotBuilders.report,
|
||||
documentService: renderers.reportRenderer,
|
||||
documentService: documentGeneratorPipeline,
|
||||
transactionManager,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
DocumentGenerationService,
|
||||
DocumentPostProcessorChain,
|
||||
type FastReportRenderer,
|
||||
type FastReportTemplateResolver,
|
||||
type IDocumentCacheStore,
|
||||
type IDocumentPostProcessor,
|
||||
type IDocumentSideEffect,
|
||||
@ -28,7 +29,7 @@ import { PersistIssuedInvoiceDocumentSideEffect } from "../side-effects";
|
||||
export interface IssuedInvoiceDocumentPipelineFactoryDeps {
|
||||
// Core / Infra
|
||||
fastReportRenderer: FastReportRenderer;
|
||||
//templateResolver: IDocumentTemplateResolver;
|
||||
templateResolver: FastReportTemplateResolver;
|
||||
|
||||
signingContextResolver: ISigningContextResolver;
|
||||
documentSigningService: IDocumentSigningService;
|
||||
@ -49,7 +50,7 @@ export class IssuedInvoiceDocumentPipelineFactory {
|
||||
// 2. Renderer (FastReport)
|
||||
const documentRenderer = new IssuedInvoiceDocumentRenderer(
|
||||
deps.fastReportRenderer,
|
||||
"/templates/issued-invoice.frx"
|
||||
deps.templateResolver
|
||||
);
|
||||
|
||||
// 3) Metadata factory (Application)
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import type { FastReportRenderer, IDocument, IDocumentRenderer } from "@erp/core/api";
|
||||
import type {
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
IDocument,
|
||||
IDocumentRenderer,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { IssuedInvoiceReportSnapshot } from "../../../../application";
|
||||
|
||||
@ -12,17 +17,32 @@ import type { IssuedInvoiceReportSnapshot } from "../../../../application";
|
||||
*
|
||||
* NO captura errores: FastReportError se propaga.
|
||||
*/
|
||||
|
||||
export type IssuedInvoiceDocumentRenderParams = {
|
||||
companySlug: string;
|
||||
};
|
||||
|
||||
export class IssuedInvoiceDocumentRenderer
|
||||
implements IDocumentRenderer<IssuedInvoiceReportSnapshot>
|
||||
{
|
||||
constructor(
|
||||
private readonly fastReportRenderer: FastReportRenderer,
|
||||
private readonly templatePath: string
|
||||
private readonly templateResolver: FastReportTemplateResolver
|
||||
) {}
|
||||
|
||||
async render(snapshot: IssuedInvoiceReportSnapshot): Promise<IDocument> {
|
||||
async render(
|
||||
snapshot: IssuedInvoiceReportSnapshot,
|
||||
params: IssuedInvoiceDocumentRenderParams
|
||||
): Promise<IDocument> {
|
||||
const { companySlug } = params;
|
||||
const templatePath = this.templateResolver.resolveTemplatePath(
|
||||
"customer-invoices",
|
||||
companySlug,
|
||||
"issued-invoice.frx"
|
||||
);
|
||||
|
||||
const output = await this.fastReportRenderer.render({
|
||||
templatePath: this.templatePath,
|
||||
templatePath,
|
||||
inputData: snapshot,
|
||||
format: "PDF",
|
||||
});
|
||||
|
||||
@ -10,13 +10,13 @@ import {
|
||||
ReportIssueInvoiceByIdParamsRequestSchema,
|
||||
ReportIssueInvoiceByIdQueryRequestSchema,
|
||||
} from "../../../../common/dto";
|
||||
import { buildIssuedInvoicesDependencies } from "../../di";
|
||||
import type { IssuedInvoicesInternalDeps } from "../../di";
|
||||
|
||||
import { GetIssuedInvoiceByIdController } from "./controllers";
|
||||
import { ListIssuedInvoicesController } from "./controllers/list-issued-invoices.controller";
|
||||
import { ReportIssuedInvoiceController } from "./controllers/report-issued-invoice.controller";
|
||||
|
||||
export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
|
||||
const { app, baseRoutePath, logger } = params as {
|
||||
app: Application;
|
||||
database: Sequelize;
|
||||
@ -24,8 +24,6 @@ export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
logger: ILogger;
|
||||
};
|
||||
|
||||
const deps = buildIssuedInvoicesDependencies(params);
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||
// 🔐 Autenticación + Tenancy para TODO el router
|
||||
@ -50,7 +48,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(ListIssuedInvoicesRequestSchema, "params"),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.list_issued_invoices();
|
||||
const useCase = deps.useCases.listIssuedInvoices();
|
||||
const controller = new ListIssuedInvoicesController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -61,7 +59,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.get_issued_invoice_by_id();
|
||||
const useCase = deps.useCases.getIssuedInvoiceById();
|
||||
const controller = new GetIssuedInvoiceByIdController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -74,7 +72,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.report_issued_invoice();
|
||||
const useCase = deps.useCases.reportIssuedInvoice();
|
||||
const controller = new ReportIssuedInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import { models } from "./infrastructure";
|
||||
|
||||
@ -9,24 +9,67 @@ export const customersAPIModule: IModuleServer = {
|
||||
version: "1.0.0",
|
||||
dependencies: [],
|
||||
|
||||
async init(params: ModuleParams) {
|
||||
// const contacts = getService<ContactsService>("contacts");
|
||||
const { logger } = params;
|
||||
//customersRouter(params);
|
||||
logger.info("🚀 Customers module initialized", { label: this.name });
|
||||
},
|
||||
async registerDependencies(params) {
|
||||
const { database, logger } = params;
|
||||
/**
|
||||
* Fase de SETUP
|
||||
* ----------------
|
||||
* - Construye el dominio (una sola vez)
|
||||
* - Define qué expone el módulo
|
||||
* - NO conecta infraestructura
|
||||
*/
|
||||
async setup(params) {
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
logger.info("🚀 Customers module dependencies registered", {
|
||||
label: this.name,
|
||||
});
|
||||
return {
|
||||
// Modelos Sequelize del módulo
|
||||
models,
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
/*...*/
|
||||
customers: {
|
||||
/*...*/
|
||||
},
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
customers: {
|
||||
/*...*/
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Fase de START
|
||||
* -------------
|
||||
* - Conecta el módulo al runtime
|
||||
* - Puede usar servicios e internals ya construidos
|
||||
* - NO construye dominio
|
||||
*/
|
||||
async start(params) {
|
||||
const { app, baseRoutePath, logger, getInternal } = params;
|
||||
|
||||
// Recuperamos el dominio interno del módulo
|
||||
const customersInternalDeps = getInternal("customers", "customers");
|
||||
|
||||
// Registro de rutas HTTP
|
||||
//customersRouter(params, customersInternalDeps);
|
||||
|
||||
logger.info("🚀 Customers module started", {
|
||||
label: this.name,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Warmup opcional (si lo necesitas en el futuro)
|
||||
* ----------------------------------------------
|
||||
* warmup(params) {
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
};
|
||||
|
||||
export default customersAPIModule;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user