diff --git a/apps/server/src/register-modules.ts b/apps/server/src/register-modules.ts index a20cc3b5..153abd1b 100644 --- a/apps/server/src/register-modules.ts +++ b/apps/server/src/register-modules.ts @@ -1,8 +1,9 @@ import customerInvoicesAPIModule from "@erp/customer-invoices/api"; -import customersAPIModule from "@erp/customers/api"; //import verifactuAPIModule from "@erp/verifactu/api"; +import customersAPIModule from "@erp/customers/api"; + import { registerModule } from "./lib"; export const registerModules = () => { diff --git a/biome.json b/biome.json index 5e649d87..42f8d8c8 100644 --- a/biome.json +++ b/biome.json @@ -66,7 +66,9 @@ "options": { "strictCase": false, "requireAscii": true, - "filenameCases": ["kebab-case"] + "filenameCases": [ + "kebab-case" + ] } }, "useForOf": "error", @@ -81,19 +83,28 @@ "selector": { "kind": "function" }, - "formats": ["camelCase", "PascalCase"] + "formats": [ + "camelCase", + "PascalCase" + ] }, { "selector": { "kind": "variable" }, - "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] + "formats": [ + "camelCase", + "PascalCase", + "CONSTANT_CASE" + ] }, { "selector": { "kind": "typeLike" }, - "formats": ["PascalCase"] + "formats": [ + "PascalCase" + ] } ] } @@ -169,7 +180,7 @@ } }, "noForEach": "warn", - "noStaticOnlyClass": "error", + "noStaticOnlyClass": "off", "noThisInStatic": "error", "noUselessCatch": "error", "noUselessConstructor": "error", @@ -182,7 +193,7 @@ "noVoid": "error", "useFlatMap": "error", "useLiteralKeys": "error", - "useOptionalChain": "error", + "useOptionalChain": "off", "useSimpleNumberKeys": "error", "useSimplifiedLogicExpression": "info" }, @@ -234,21 +245,46 @@ "groups": [ ":URL:", ":BLANK_LINE:", - [":BUN:", ":NODE:"], + [ + ":BUN:", + ":NODE:" + ], ":BLANK_LINE:", ":PACKAGE_WITH_PROTOCOL:", ":BLANK_LINE:", - [":PACKAGE:"], + [ + ":PACKAGE:" + ], ":BLANK_LINE:", - ["!@/**", "!#*", "!~*", "!$*", "!%*"], + [ + "!@/**", + "!#*", + "!~*", + "!$*", + "!%*" + ], ":BLANK_LINE:", - ["@/**", "#*", "~*", "$*", "%*"], + [ + "@/**", + "#*", + "~*", + "$*", + "%*" + ], ":BLANK_LINE:", - [":PATH:", "!./**", "!../**"], + [ + ":PATH:", + "!./**", + "!../**" + ], ":BLANK_LINE:", - ["../**"], + [ + "../**" + ], ":BLANK_LINE:", - ["./**"] + [ + "./**" + ] ], "identifierOrder": "lexicographic" } @@ -268,7 +304,12 @@ "semicolons": "always", "trailingCommas": "es5" }, - "globals": ["console", "process", "__dirname", "__filename"] + "globals": [ + "console", + "process", + "__dirname", + "__filename" + ] }, "json": { "formatter": { @@ -302,7 +343,11 @@ }, "overrides": [ { - "includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"], + "includes": [ + "**/*.test.{js,ts,tsx}", + "**/*.spec.{js,ts,tsx}", + "**/__tests__/**" + ], "linter": { "rules": { "suspicious": { @@ -316,7 +361,11 @@ } }, { - "includes": ["**/next.config.{js,ts}", "**/tailwind.config.{js,ts}", "**/*.config.{js,ts}"], + "includes": [ + "**/next.config.{js,ts}", + "**/tailwind.config.{js,ts}", + "**/*.config.{js,ts}" + ], "linter": { "rules": { "style": { @@ -329,7 +378,11 @@ } }, { - "includes": ["**/pages/**", "**/app/**/page.{tsx,jsx}", "**/app/**/layout.{tsx,jsx}"], + "includes": [ + "**/pages/**", + "**/app/**/page.{tsx,jsx}", + "**/app/**/layout.{tsx,jsx}" + ], "linter": { "rules": { "style": { @@ -339,7 +392,10 @@ } }, { - "includes": ["**/*.d.ts", "**/lib/env/*.ts"], + "includes": [ + "**/*.d.ts", + "**/lib/env/*.ts" + ], "linter": { "rules": { "style": { @@ -353,7 +409,10 @@ "selector": { "kind": "objectLiteralProperty" }, - "formats": ["CONSTANT_CASE", "camelCase"] + "formats": [ + "CONSTANT_CASE", + "camelCase" + ] } ] } @@ -374,4 +433,4 @@ } } ] -} +} \ No newline at end of file diff --git a/modules/core/src/api/application/documents/application-models/company-certificate-context.interface.ts b/modules/core/src/api/application/documents/application-models/company-certificate-context.interface.ts new file mode 100644 index 00000000..0f0c459f --- /dev/null +++ b/modules/core/src/api/application/documents/application-models/company-certificate-context.interface.ts @@ -0,0 +1,18 @@ +/** + * Contexto de certificados de una empresa para firma digital. + * + * - NO contiene certificados ni passwords + * - Contiene referencias a secretos externos (Infisical, etc.) + * - Application-level + */ + +export interface ICompanyCertificateContext { + /** Nombre del secreto que contiene el certificado */ + readonly certificateSecretName: string; + + /** Nombre del secreto que contiene la password del certificado */ + readonly certificatePasswordSecretName: string; + + /** Identificador lógico de la empresa */ + readonly companySlug: string; +} diff --git a/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts b/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts new file mode 100644 index 00000000..8c314467 --- /dev/null +++ b/modules/core/src/api/application/documents/application-models/document-metadata.interface.ts @@ -0,0 +1,9 @@ +export interface IDocumentMetadata { + readonly documentType: string; + readonly documentId: string; + readonly companyId: string; + readonly format: "PDF" | "HTML"; + readonly languageCode: string; + readonly filename: string; + readonly cacheKey?: string; // opcional +} diff --git a/modules/core/src/api/application/documents/application-models/document.interface.ts b/modules/core/src/api/application/documents/application-models/document.interface.ts new file mode 100644 index 00000000..e337d638 --- /dev/null +++ b/modules/core/src/api/application/documents/application-models/document.interface.ts @@ -0,0 +1,5 @@ +export interface IDocument { + readonly payload: Buffer; + readonly mimeType: string; + readonly filename: string; +} diff --git a/modules/core/src/api/application/documents/application-models/index.ts b/modules/core/src/api/application/documents/application-models/index.ts new file mode 100644 index 00000000..134f1282 --- /dev/null +++ b/modules/core/src/api/application/documents/application-models/index.ts @@ -0,0 +1,3 @@ +export * from "./company-certificate-context.interface"; +export * from "./document.interface"; +export * from "./document-metadata.interface"; diff --git a/modules/core/src/api/application/documents/errors/document-generation-error.ts b/modules/core/src/api/application/documents/errors/document-generation-error.ts new file mode 100644 index 00000000..59b5a3d0 --- /dev/null +++ b/modules/core/src/api/application/documents/errors/document-generation-error.ts @@ -0,0 +1,80 @@ +import { ApplicationError } from "@repo/rdx-ddd"; + +type DocumentGenerationErrorCode = + | "METADATA_ERROR" + | "CACHE_ERROR" // no fatal (solo para métricas/logs si se usa) + | "RENDER_ERROR" + | "POST_PROCESS_ERROR" + | "SIDE_EFFECT_PROCESS_ERROR" + | "SIGNING_ERROR" + | "SECURITY_ERROR" + | "CONFIGURATION_ERROR" + | "INFRASTRUCTURE_ERROR"; + +export const isDocumentGenerationError = (e: unknown): e is DocumentGenerationError => + e instanceof DocumentGenerationError; + +export class DocumentGenerationError extends ApplicationError { + readonly kind = "document-generation"; + + private constructor( + message: string, + code: DocumentGenerationErrorCode, + cause?: unknown, + metadata?: Record + ) { + super(message, code, { cause, metadata }); + } + + static metadata(cause: unknown) { + return new DocumentGenerationError("Invalid document metadata", "METADATA_ERROR", cause); + } + + static render(cause: unknown) { + return new DocumentGenerationError("Document render failed", "RENDER_ERROR", cause); + } + + static postProcess(cause: unknown) { + return new DocumentGenerationError( + "Document post-processing failed", + "POST_PROCESS_ERROR", + cause + ); + } + + static sideEffect(cause: unknown) { + return new DocumentGenerationError( + "Document side effect failed", + "SIDE_EFFECT_PROCESS_ERROR", + cause + ); + } + + static signing(cause: unknown) { + return new DocumentGenerationError("Document signing failed", "SIGNING_ERROR", cause); + } + + static security(cause: unknown) { + return new DocumentGenerationError( + "Security error during document generation", + "SECURITY_ERROR", + cause + ); + } + + static configuration(cause: unknown) { + return new DocumentGenerationError( + "Document generation misconfiguration", + "CONFIGURATION_ERROR", + cause + ); + } + + static infrastructure(cause: unknown) { + return new DocumentGenerationError( + "Infrastructure error during document generation", + "INFRASTRUCTURE_ERROR", + cause + ); + } +} diff --git a/modules/core/src/api/application/documents/errors/index.ts b/modules/core/src/api/application/documents/errors/index.ts new file mode 100644 index 00000000..b40739ee --- /dev/null +++ b/modules/core/src/api/application/documents/errors/index.ts @@ -0,0 +1 @@ +export * from "./document-generation-error"; diff --git a/modules/core/src/api/application/documents/index.ts b/modules/core/src/api/application/documents/index.ts new file mode 100644 index 00000000..aea907a4 --- /dev/null +++ b/modules/core/src/api/application/documents/index.ts @@ -0,0 +1,2 @@ +export * from "./application-models"; +export * from "./services"; diff --git a/modules/core/src/api/application/documents/services/document-cache-key-factory.ts b/modules/core/src/api/application/documents/services/document-cache-key-factory.ts new file mode 100644 index 00000000..9b59b558 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-cache-key-factory.ts @@ -0,0 +1,16 @@ +import type { IDocumentMetadata } from "../application-models"; + +export class DocumentCacheKeyFactory { + static fromMetadata(metadata: IDocumentMetadata): string { + return; + return [ + metadata.documentType, + metadata.companyId, + metadata.documentId, + metadata.format, + metadata.languageCode, + metadata.filename, + metadata.cacheKey, + ].join(":"); + } +} diff --git a/modules/core/src/api/application/documents/services/document-cache.interface.ts b/modules/core/src/api/application/documents/services/document-cache.interface.ts new file mode 100644 index 00000000..2545c8de --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-cache.interface.ts @@ -0,0 +1,17 @@ +import type { IDocument } from "@erp/core/api/application"; + +export interface IDocumentCacheStore { + /** + * Devuelve el documento firmado si existe y es válido. + * - null => no existe / no válido + * - nunca lanza por errores técnicos (best-effort) + */ + get(cacheKey: string): Promise; + + /** + * Guarda un documento firmado en cache. + * - best-effort + * - errores se ignoran o se loguean + */ + set(cacheKey: string, document: IDocument): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-generation-error.ts b/modules/core/src/api/application/documents/services/document-generation-error.ts new file mode 100644 index 00000000..9fe1b9c0 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-generation-error.ts @@ -0,0 +1,61 @@ +import { ApplicationError } from "@repo/rdx-ddd"; + +export type DocumentGenerationErrorType = + | "METADATA" + | "PRE_PROCESS" + | "RENDER" + | "POST_PROCESS" + | "SIDE_EFFECT"; + +export class DocumentGenerationError extends ApplicationError { + public readonly documentErrorType: DocumentGenerationErrorType; + + private constructor( + type: DocumentGenerationErrorType, + message: string, + options?: ErrorOptions & { metadata?: Record } + ) { + super(message, "DOCUMENT_GENERATION_ERROR", options); + this.documentErrorType = type; + } + + // ---- factories (único punto de creación) ---- + + static metadata(error: unknown, metadata?: Record): DocumentGenerationError { + return new DocumentGenerationError("METADATA", "Failed to build document metadata", { + cause: error, + metadata, + }); + } + + static preProcess(error: unknown, metadata?: Record): DocumentGenerationError { + return new DocumentGenerationError("PRE_PROCESS", "Failed during document pre-processing", { + cause: error, + metadata, + }); + } + + static render(error: unknown, metadata?: Record): DocumentGenerationError { + return new DocumentGenerationError("RENDER", "Failed to render document", { + cause: error, + metadata, + }); + } + + static postProcess(error: unknown, metadata?: Record): DocumentGenerationError { + return new DocumentGenerationError("POST_PROCESS", "Failed during document post-processing", { + cause: error, + metadata, + }); + } + + static sideEffect(error: unknown, metadata?: Record): DocumentGenerationError { + return new DocumentGenerationError("SIDE_EFFECT", "Failed during document side-effects", { + cause: error, + metadata, + }); + } +} + +export const isDocumentGenerationError = (e: unknown): e is DocumentGenerationError => + e instanceof DocumentGenerationError; diff --git a/modules/core/src/api/application/documents/services/document-generation-service.ts b/modules/core/src/api/application/documents/services/document-generation-service.ts new file mode 100644 index 00000000..abf5eebc --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-generation-service.ts @@ -0,0 +1,91 @@ +import { logger } from "@erp/core/api"; +import { Result } from "@repo/rdx-utils"; + +import type { IDocument, IDocumentMetadata } from "../application-models"; +import { DocumentGenerationError } from "../errors"; + +import type { IDocumentMetadataFactory } from "./document-metadata-factory.interface"; +import type { IDocumentPostProcessor } from "./document-post-processor.interface"; +import type { IDocumentPreProcessor } from "./document-pre-processor.interface"; +import type { IDocumentRenderer } from "./document-renderer.interface"; +import type { IDocumentSideEffect } from "./document-side-effect.interface"; + +/** + * Servicio de Application que orquesta la generación de documentos. + * + * Flujo inmutable: + * 1. Construcción de metadata + * 2. Pre-processors (short-circuit: cache, idempotencia) + * 3. Render + * 4. Post-processors (firma, watermark, etc.) + * 5. Side-effects (persistencia, métricas) [best-effort] + */ +export class DocumentGenerationService { + constructor( + private readonly metadataFactory: IDocumentMetadataFactory, + private readonly preProcessors: readonly IDocumentPreProcessor[], + private readonly renderer: IDocumentRenderer, + private readonly postProcessor: IDocumentPostProcessor, + private readonly sideEffects: readonly IDocumentSideEffect[] + ) {} + + async generate(snapshot: TSnapshot): Promise> { + let metadata: IDocumentMetadata; + + // 1. Metadata + try { + metadata = this.metadataFactory.build(snapshot); + } catch (error) { + return Result.fail(DocumentGenerationError.metadata(error)); + } + + // 2. Pre-processors (cache / short-circuit) + for (const preProcessor of this.preProcessors) { + try { + const cached = await preProcessor.tryResolve(metadata); + if (cached) { + return Result.ok(cached); + } + } catch (error) { + // best-effort: ignorar y continuar + } + } + + // 3. Render + let document: IDocument; + try { + document = await this.renderer.render(snapshot, { + format: metadata.format, + languageCode: metadata.languageCode, + filename: metadata.filename, + mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html", + }); + } catch (error) { + return Result.fail(DocumentGenerationError.render(error)); + } + + // 4. Post-processors (transformaciones) + if (this.postProcessor) { + try { + document = await this.postProcessor.process(document, metadata); + } catch (error) { + return Result.fail(DocumentGenerationError.postProcess(error)); + } + } + + // 5. Side-effects (best-effort) + for (const effect of this.sideEffects) { + try { + await effect.execute(document, metadata); + } catch (err) { + // Importante: + // - Nunca rompe el flujo + // - Logging a nivel infra / observabilidad + const error = DocumentGenerationError.sideEffect(err as Error); + logger.warn(error.message, { error }); + } + } + + return Result.ok(document); + } +} diff --git a/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts b/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts new file mode 100644 index 00000000..846dede0 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-metadata-factory.interface.ts @@ -0,0 +1,5 @@ +import type { IDocumentMetadata } from "../application-models"; + +export interface IDocumentMetadataFactory { + build(snapshot: TSnapshot): IDocumentMetadata; +} diff --git a/modules/core/src/api/application/documents/services/document-post-processor-chain.ts b/modules/core/src/api/application/documents/services/document-post-processor-chain.ts new file mode 100644 index 00000000..818f1746 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-post-processor-chain.ts @@ -0,0 +1,17 @@ +import type { IDocument, IDocumentMetadata } from "../application-models"; + +import type { IDocumentPostProcessor } from "./document-post-processor.interface"; + +export class DocumentPostProcessorChain implements IDocumentPostProcessor { + constructor(private readonly processors: readonly IDocumentPostProcessor[]) {} + + async process(document: IDocument, metadata: IDocumentMetadata): Promise { + let current = document; + + for (const processor of this.processors) { + current = await processor.process(current, metadata); + } + + return current; + } +} diff --git a/modules/core/src/api/application/documents/services/document-post-processor.interface.ts b/modules/core/src/api/application/documents/services/document-post-processor.interface.ts new file mode 100644 index 00000000..0b4d65fa --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-post-processor.interface.ts @@ -0,0 +1,9 @@ +import type { IDocument, IDocumentMetadata } from "../application-models"; + +export interface IDocumentPostProcessor { + /** + * Transforma el documento. + * Debe devolver un nuevo IDocument (inmutabilidad). + */ + process(document: IDocument, metadata: IDocumentMetadata): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-pre-processor.interface.ts b/modules/core/src/api/application/documents/services/document-pre-processor.interface.ts new file mode 100644 index 00000000..e685a957 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-pre-processor.interface.ts @@ -0,0 +1,11 @@ +import type { IDocument, IDocumentMetadata } from "../application-models"; + +export interface IDocumentPreProcessor { + /** + * Intenta resolver el documento final. + * - Devuelve IDocument → corta el pipeline + * - Devuelve null → continuar + * - Lanza → error de Application (normalizado por el orquestador) + */ + tryResolve(metadata: IDocumentMetadata): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-renderer.interface.ts b/modules/core/src/api/application/documents/services/document-renderer.interface.ts new file mode 100644 index 00000000..7c5ff186 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-renderer.interface.ts @@ -0,0 +1,5 @@ +import type { IDocument } from "../application-models"; + +export interface IDocumentRenderer { + render(source: TSource, params?: Record): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-side-effect.interface.ts b/modules/core/src/api/application/documents/services/document-side-effect.interface.ts new file mode 100644 index 00000000..f4dab11c --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-side-effect.interface.ts @@ -0,0 +1,10 @@ +// core/api/application/documents/services/document-side-effect.ts +import type { IDocument, IDocumentMetadata } from "../application-models"; + +export interface IDocumentSideEffect { + /** + * Ejecuta un efecto secundario (persistencia, métricas, auditoría). + * Los errores deben ser capturados por el orquestador. + */ + execute(document: IDocument, metadata: IDocumentMetadata): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-signing-service.interface.ts b/modules/core/src/api/application/documents/services/document-signing-service.interface.ts new file mode 100644 index 00000000..dcebcbf0 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-signing-service.interface.ts @@ -0,0 +1,5 @@ +import type { ICompanyCertificateContext } from "../application-models"; + +export interface IDocumentSigningService { + sign(payload: Buffer, context: ICompanyCertificateContext): Promise; +} diff --git a/modules/core/src/api/application/documents/services/document-storage.interface.ts b/modules/core/src/api/application/documents/services/document-storage.interface.ts new file mode 100644 index 00000000..d36d9d07 --- /dev/null +++ b/modules/core/src/api/application/documents/services/document-storage.interface.ts @@ -0,0 +1,12 @@ +import type { IDocument, IDocumentMetadata } from "../application-models"; + +export interface IDocumentStorage { + /** + * Persiste un documento generado. + * + * - Side-effect + * - Best-effort + * - Nunca lanza (errores se gestionan internamente) + */ + save(document: IDocument, metadata: IDocumentMetadata): Promise; +} diff --git a/modules/core/src/api/application/documents/services/index.ts b/modules/core/src/api/application/documents/services/index.ts new file mode 100644 index 00000000..15e366e8 --- /dev/null +++ b/modules/core/src/api/application/documents/services/index.ts @@ -0,0 +1,13 @@ +export * from "./document-cache.interface"; +export * from "./document-cache-key-factory"; +export * from "./document-generation-error"; +export * from "./document-generation-service"; +export * from "./document-metadata-factory.interface"; +export * from "./document-post-processor.interface"; +export * from "./document-post-processor-chain"; +export * from "./document-pre-processor.interface"; +export * from "./document-renderer.interface"; +export * from "./document-side-effect.interface"; +export * from "./document-signing-service.interface"; +export * from "./document-storage.interface"; +export * from "./signing-context-resolver.interface"; diff --git a/modules/core/src/api/application/documents/services/signing-context-resolver.interface.ts b/modules/core/src/api/application/documents/services/signing-context-resolver.interface.ts new file mode 100644 index 00000000..b26286b2 --- /dev/null +++ b/modules/core/src/api/application/documents/services/signing-context-resolver.interface.ts @@ -0,0 +1,9 @@ +import type { ICompanyCertificateContext } from "../application-models"; + +export interface ISigningContextResolver { + /** + * Resuelve el contexto de firma para una empresa. + * Lanza si no está configurado. + */ + resolveForCompany(companyId: string): Promise; +} diff --git a/modules/core/src/api/application/errors/application-error.ts b/modules/core/src/api/application/errors/application-error.ts deleted file mode 100644 index ee3dd16a..00000000 --- a/modules/core/src/api/application/errors/application-error.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Errores de capa de aplicación. No deben "filtrarse" a cliente tal cual. - * - * */ - -export class ApplicationError extends Error { - public readonly layer = "application" as const; - constructor(message: string, options?: ErrorOptions) { - super(message, options); - Object.setPrototypeOf(this, new.target.prototype); - } -} diff --git a/modules/core/src/api/application/errors/index.ts b/modules/core/src/api/application/errors/index.ts deleted file mode 100644 index 7636f588..00000000 --- a/modules/core/src/api/application/errors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./application-error"; diff --git a/modules/core/src/api/application/index.ts b/modules/core/src/api/application/index.ts index c7df6428..892bc0c0 100644 --- a/modules/core/src/api/application/index.ts +++ b/modules/core/src/api/application/index.ts @@ -1,3 +1,3 @@ -export * from "./errors"; -export * from "./presenters"; +export * from "./documents"; export * from "./renderers"; +export * from "./snapshot-builders"; diff --git a/modules/core/src/api/application/presenters/presenter.interface.ts b/modules/core/src/api/application/presenters/presenter.interface.ts deleted file mode 100644 index ca0605bb..00000000 --- a/modules/core/src/api/application/presenters/presenter.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { DTO } from "../../../common/types"; - -export type IPresenterOutputParams = Record; - -export interface IPresenter { - toOutput(source: TSource, params?: IPresenterOutputParams): TOutput | Promise; -} diff --git a/modules/core/src/api/application/presenters/presenter.ts b/modules/core/src/api/application/presenters/presenter.ts deleted file mode 100644 index ef950980..00000000 --- a/modules/core/src/api/application/presenters/presenter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IPresenter, IPresenterOutputParams } from "./presenter.interface"; -import type { IPresenterRegistry } from "./presenter-registry.interface"; - -export abstract class Presenter - implements IPresenter -{ - constructor(protected presenterRegistry: IPresenterRegistry) { - // - } - abstract toOutput(source: TSource, params?: IPresenterOutputParams): TOutput; -} diff --git a/modules/core/src/api/application/renderers/renderer-registry.ts b/modules/core/src/api/application/renderers/renderer-registry.ts index 7bb21c78..c618d36f 100644 --- a/modules/core/src/api/application/renderers/renderer-registry.ts +++ b/modules/core/src/api/application/renderers/renderer-registry.ts @@ -1,4 +1,4 @@ -import { ApplicationError } from "../errors"; +import { ApplicationError } from "@repo/rdx-ddd"; import type { IRenderer } from "./renderer.interface"; import type { IRendererRegistry, RendererKey } from "./renderer-registry.interface"; diff --git a/modules/core/src/api/application/presenters/index.ts b/modules/core/src/api/application/snapshot-builders/index.ts similarity index 51% rename from modules/core/src/api/application/presenters/index.ts rename to modules/core/src/api/application/snapshot-builders/index.ts index 91069846..4d6e02c9 100644 --- a/modules/core/src/api/application/presenters/index.ts +++ b/modules/core/src/api/application/snapshot-builders/index.ts @@ -1,4 +1,4 @@ -export * from "./presenter"; -export * from "./presenter.interface"; export * from "./presenter-registry"; export * from "./presenter-registry.interface"; +export * from "./snapshot-builder"; +export * from "./snapshot-builder.interface"; diff --git a/modules/core/src/api/application/presenters/presenter-registry.interface.ts b/modules/core/src/api/application/snapshot-builders/presenter-registry.interface.ts similarity index 75% rename from modules/core/src/api/application/presenters/presenter-registry.interface.ts rename to modules/core/src/api/application/snapshot-builders/presenter-registry.interface.ts index 1530f8b1..36cd0e19 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.interface.ts +++ b/modules/core/src/api/application/snapshot-builders/presenter-registry.interface.ts @@ -1,6 +1,6 @@ import type { DTO } from "@erp/core/common"; -import type { IPresenter } from "./presenter.interface"; +import type { ISnapshotBuilder } from "./snapshot-builder.interface"; /** * 🔑 Claves de proyección comunes para seleccionar presenters @@ -25,17 +25,17 @@ export interface IPresenterRegistry { */ getPresenter( key: Omit & { format?: F } - ): IPresenter; + ): ISnapshotBuilder; /** * Registra un mapper de dominio bajo una clave de proyección. */ registerPresenter( key: PresenterKey, - presenter: IPresenter + presenter: ISnapshotBuilder ): this; registerPresenters( - presenters: Array<{ key: PresenterKey; presenter: IPresenter }> + presenters: Array<{ key: PresenterKey; presenter: ISnapshotBuilder }> ): this; } diff --git a/modules/core/src/api/application/presenters/presenter-registry.ts b/modules/core/src/api/application/snapshot-builders/presenter-registry.ts similarity index 84% rename from modules/core/src/api/application/presenters/presenter-registry.ts rename to modules/core/src/api/application/snapshot-builders/presenter-registry.ts index 1d80c9ce..1e7fcb90 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.ts +++ b/modules/core/src/api/application/snapshot-builders/presenter-registry.ts @@ -1,10 +1,10 @@ -import { ApplicationError } from "../errors"; +import { ApplicationError } from "@repo/rdx-ddd"; -import type { IPresenter } from "./presenter.interface"; import type { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface"; +import type { ISnapshotBuilder } from "./snapshot-builder.interface"; export class InMemoryPresenterRegistry implements IPresenterRegistry { - private registry: Map> = new Map(); + private registry: Map> = new Map(); private _normalizeKey(key: PresenterKey): PresenterKey { return { @@ -29,13 +29,13 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { private _registerPresenter( key: PresenterKey, - presenter: IPresenter + presenter: ISnapshotBuilder ): void { const exactKey = this._buildKey(key); this.registry.set(exactKey, presenter); } - getPresenter(key: PresenterKey): IPresenter { + getPresenter(key: PresenterKey): ISnapshotBuilder { const exactKey = this._buildKey(key); // 1) Intentar clave exacta @@ -76,7 +76,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { registerPresenter( key: PresenterKey, - presenter: IPresenter + presenter: ISnapshotBuilder ): this { this._registerPresenter(key, presenter); return this; @@ -85,7 +85,7 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { * ✅ Registro en lote de presentadores. */ registerPresenters( - presenters: Array<{ key: PresenterKey; presenter: IPresenter }> + presenters: Array<{ key: PresenterKey; presenter: ISnapshotBuilder }> ): this { for (const { key, presenter } of presenters) { this._registerPresenter(key, presenter); diff --git a/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts b/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts new file mode 100644 index 00000000..4bae855c --- /dev/null +++ b/modules/core/src/api/application/snapshot-builders/snapshot-builder.interface.ts @@ -0,0 +1,5 @@ +export type ISnapshotBuilderParams = Readonly>; + +export interface ISnapshotBuilder { + toOutput(source: TSource, params?: ISnapshotBuilderParams): TSnapshot; +} diff --git a/modules/core/src/api/application/snapshot-builders/snapshot-builder.ts b/modules/core/src/api/application/snapshot-builders/snapshot-builder.ts new file mode 100644 index 00000000..0b0b1737 --- /dev/null +++ b/modules/core/src/api/application/snapshot-builders/snapshot-builder.ts @@ -0,0 +1,11 @@ +import type { IPresenterRegistry } from "./presenter-registry.interface"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "./snapshot-builder.interface"; + +export abstract class SnapshotBuilder + implements ISnapshotBuilder +{ + constructor(protected snapshotBuilderRegistry: IPresenterRegistry) { + // + } + abstract toOutput(source: TSource, params?: ISnapshotBuilderParams): TOutput; +} diff --git a/modules/core/src/api/infrastructure/di/documents.di.ts b/modules/core/src/api/infrastructure/di/documents.di.ts new file mode 100644 index 00000000..dc839211 --- /dev/null +++ b/modules/core/src/api/infrastructure/di/documents.di.ts @@ -0,0 +1,45 @@ +import { + EnvCompanySigningContextResolver, + FastReportExecutableResolver, + FastReportProcessRunner, + FastReportRenderer, + FilesystemDocumentCacheStore, + RestDocumentSigningService, +} from "../documents"; +import { FilesystemDocumentStorage } from "../storage"; + +export function buildCoreDocumentsDI(env: NodeJS.ProcessEnv) { + // Renderers + const frExecutableResolver = new FastReportExecutableResolver(env.FASTREPORT_BIN); + const frProcessRunner = new FastReportProcessRunner(); + const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner); + + // Signing + const signingContextResolver = new EnvCompanySigningContextResolver(env); + + const signingService = new RestDocumentSigningService({ + signUrl: String(env.SIGNING_BASE_URL), + timeoutMs: env.SIGNING_TIMEOUT_MS ? Number.parseInt(env.SIGNING_TIMEOUT_MS, 10) : 15_000, + maxRetries: env.SIGNING_MAX_RETRIES ? Number.parseInt(env.SIGNING_MAX_RETRIES, 10) : 2, + }); + + // Cache para documentos firmados + const cacheStore = new FilesystemDocumentCacheStore(String(env.SIGNED_DOCUMENTS_CACHE_PATH)); + + // Almancenamiento para documentos firmados + const storage = new FilesystemDocumentStorage(String(env.SIGNED_DOCUMENTS_PATH)); + + return { + documentRenderers: { + fastReportRenderer, + }, + documentSigning: { + signingService, + signingContextResolver, + }, + documentStorage: { + cacheStore, + storage, + }, + }; +} diff --git a/modules/core/src/api/infrastructure/di/index.ts b/modules/core/src/api/infrastructure/di/index.ts new file mode 100644 index 00000000..be783695 --- /dev/null +++ b/modules/core/src/api/infrastructure/di/index.ts @@ -0,0 +1,2 @@ +export * from "./documents.di"; +export * from "./transactions.di"; diff --git a/modules/core/src/api/infrastructure/di/transactions.di.ts b/modules/core/src/api/infrastructure/di/transactions.di.ts new file mode 100644 index 00000000..2ac72836 --- /dev/null +++ b/modules/core/src/api/infrastructure/di/transactions.di.ts @@ -0,0 +1,6 @@ +import type { Sequelize } from "sequelize"; + +import { SequelizeTransactionManager } from "../sequelize"; + +export const buildTransactionManager = (database: Sequelize) => + new SequelizeTransactionManager(database); diff --git a/modules/core/src/api/infrastructure/documents/certificates/company-signing-context-record.interface.ts b/modules/core/src/api/infrastructure/documents/certificates/company-signing-context-record.interface.ts new file mode 100644 index 00000000..b4a2745b --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/certificates/company-signing-context-record.interface.ts @@ -0,0 +1,5 @@ +export interface ICompanySigningContextRecord { + certificateId: string; + certificateSecretName: string; + certificatePasswordSecretName: string; +} diff --git a/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts new file mode 100644 index 00000000..a9f5e641 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/certificates/env-company-signing-context-store.ts @@ -0,0 +1,73 @@ +// core/api/infrastructure/documents/certificates/ +// env-company-signing-context-store.ts + +import type { + ICompanyCertificateContext, + ISigningContextResolver, +} from "@erp/core/api/application"; + +import type { ICompanySigningContextRecord } from "./company-signing-context-record.interface"; + +/** + * Implementación de ICompanySigningContextStore basada en + * variables de entorno. + * + * - Infra pura + * - Sin IO externo + * - Ideal para dev / staging / setups simples + */ +export class EnvCompanySigningContextResolver implements ISigningContextResolver { + private readonly records: Record; + + constructor(env: NodeJS.ProcessEnv) { + const raw = env.COMPANY_CERTIFICATES_JSON; + + if (!raw) { + this.records = {}; + return; + } + + try { + const parsed = JSON.parse(raw) as Record< + string, + { + certificateId: string; + certificateSecretName: string; + certificatePasswordSecretName: string; + } + >; + + this.records = Object.fromEntries( + Object.entries(parsed).map(([companySlug, cfg]) => [ + companySlug, + { + certificateId: cfg.certificateId, + certificateSecretName: cfg.certificateSecretName, + certificatePasswordSecretName: cfg.certificatePasswordSecretName, + }, + ]) + ); + } catch { + throw new Error("Invalid COMPANY_CERTIFICATES_JSON format"); + } + } + + async resolveForCompany(companyId: string): Promise { + /** + * En esta implementación: + * - companyId === companySlug + * - No hay lookup adicional + */ + const record = this.records[companyId]; + + if (!record) { + return null; + } + + return { + companySlug: companyId, + certificateSecretName: record.certificateSecretName, + certificatePasswordSecretName: record.certificatePasswordSecretName, + }; + } +} diff --git a/modules/core/src/api/infrastructure/documents/certificates/index.ts b/modules/core/src/api/infrastructure/documents/certificates/index.ts new file mode 100644 index 00000000..dbec57fa --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/certificates/index.ts @@ -0,0 +1,2 @@ +export * from "./company-signing-context-record.interface"; +export * from "./env-company-signing-context-store"; diff --git a/modules/core/src/api/infrastructure/documents/index.ts b/modules/core/src/api/infrastructure/documents/index.ts new file mode 100644 index 00000000..cd87e61e --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/index.ts @@ -0,0 +1,4 @@ +export * from "./certificates"; +export * from "./renderers"; +export * from "./signing"; +export * from "./storage"; diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-errors.ts similarity index 96% rename from modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts rename to modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-errors.ts index 7984ea9b..737b5222 100644 --- a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-errors.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-errors.ts @@ -1,4 +1,4 @@ -import { InfrastructureError } from "../../errors"; +import { InfrastructureError } from "../../../errors"; /** * Error base de FastReport. diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-executable-resolver.ts similarity index 89% rename from modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts rename to modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-executable-resolver.ts index 155b2f32..fa5638cd 100644 --- a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-executable-resolver.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-executable-resolver.ts @@ -15,11 +15,12 @@ import { FastReportExecutableNotFoundError } from "./fastreport-errors"; * 3. Fail-fast si no existe */ export class FastReportExecutableResolver { + constructor(private readonly execPath?: string) {} + public resolve(): string { - const fromEnv = process.env.FASTREPORT_BIN; - if (fromEnv) { - this.assertExecutableExists(fromEnv); - return fromEnv; + if (this.execPath) { + this.assertExecutableExists(this.execPath); + return this.execPath; } const executableName = this.resolveExecutableName(); diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-process-runner.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts similarity index 100% rename from modules/core/src/api/infrastructure/renderers/fast-report/fastreport-process-runner.ts rename to modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-process-runner.ts diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts similarity index 50% rename from modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts rename to modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts index f5dde145..36b59bc0 100644 --- a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-render-options.type.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-options.type.ts @@ -1,7 +1,6 @@ -import type { ReportStorageKey } from "../../reporting"; - export type FastReportRenderOptions = { + templatePath: string; inputData: unknown; format: "PDF" | "HTML"; - storageKey: ReportStorageKey; + storageKey?: string; }; diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-output.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-output.ts new file mode 100644 index 00000000..2d5c1ece --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-render-output.ts @@ -0,0 +1,4 @@ +export interface FastReportRenderOutput { + payload: Buffer | string; + templateChecksum: string; +} diff --git a/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts new file mode 100644 index 00000000..6ceae306 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/fastreport-renderer.ts @@ -0,0 +1,179 @@ +import { createHash, randomUUID } from "node:crypto"; +import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { Renderer } from "../../../../application"; + +import { FastReportExecutionError, FastReportIOError } from "./fastreport-errors"; +import type { FastReportExecutableResolver } from "./fastreport-executable-resolver"; +import type { FastReportProcessRunner } from "./fastreport-process-runner"; +import type { FastReportRenderOptions } from "./fastreport-render-options.type"; +import type { FastReportRenderOutput } from "./fastreport-render-output"; + +/** + * Clase base para renderers FastReport. + */ +export class FastReportRenderer extends Renderer { + constructor( + private readonly executableResolver: FastReportExecutableResolver, + private readonly processRunner: FastReportProcessRunner + ) { + super(); + } + + async render(options: FastReportRenderOptions): Promise { + const workDir = path.join(os.tmpdir(), "fastreport", randomUUID()); + const inputPath = path.join(workDir, "input.json"); + const outputPath = path.join(workDir, options.format === "PDF" ? "output.pdf" : "output.html"); + + await mkdir(workDir, { recursive: true }); + + try { + await this.ensureWorkDir(workDir); + await writeFile(inputPath, JSON.stringify(options.inputData), "utf-8"); + + const executablePath = this.executableResolver.resolve(); + + await this.processRunner.run(executablePath, { + templatePath: options.templatePath, + data: inputPath, + workdir: outputPath, + format: options.format, + }); + + const payload = await readFile(outputPath); + + return { + payload, + templateChecksum: await this.computeTemplateChecksum(options.templatePath), + }; + } catch (error) { + throw new FastReportExecutionError((error as Error).message); + } finally { + await this.safeCleanup(workDir); + } + } + + private async safeCleanup(path: string): Promise { + try { + await rm(path, { recursive: true, force: true }); + } catch { + // Cleanup best-effort: no throw + } + } + + private async ensureWorkDir(path: string): Promise { + try { + await import("node:fs/promises").then((fs) => fs.mkdir(path, { recursive: true })); + } catch (error) { + throw new FastReportIOError((error as Error).message); + } + } + + private async computeTemplateChecksum(templatePath: string): Promise { + try { + await access(templatePath); + const content = await readFile(templatePath); + + return createHash("sha256").update(content).digest("hex"); + } catch (error) { + throw new FastReportExecutionError((error as Error).message); + } + } +} + +/* + + protected async renderInternal( + options: FastReportRenderOptions + ): Promise { + if (process.env.NODE_ENV !== "development") { + // Cache (read-through) + const cached = await this.tryReadFromCache(options.storageKey); + if (cached.isSome()) { + return { + payload: cached.unwrap(), + templateChecksum: "CACHED", + }; + } + } + + // Resolver plantilla + const templatePath = this.resolveTemplatePath(); + console.log("Using FastReport template:", templatePath); + + if (!fs.existsSync(templatePath)) { + throw new FastReportTemplateNotFoundError(templatePath); + } + + const templateChecksum = this.calculateChecksum(templatePath); + + // Llamar a FastReport + const callResult = await this.callFastReportGenerator(options, templatePath); + + if (callResult.isFailure) { + throw callResult.error; + } + + // Guardar documento generado (best-effort) + await this.storageReport(options.storageKey, callResult.data); + + return { + payload: callResult.data, + templateChecksum, + }; + } + + protected async tryReadFromCache(docKey: ReportStorageKey): Promise> { + if (await this.reportStorage.exists(docKey)) { + const cached = await this.reportStorage.read(docKey); + return Maybe.some(cached); + } + return Maybe.none(); + } + + protected async callFastReportGenerator( + options: FastReportRenderOptions, + templatePath: string + ): Promise> { + const executablePath = this.executableResolver.resolve(); + const workdir = this.resolveWorkdir(); + + const runResult = await this.processRunner.run(executablePath, { + templatePath, + data: JSON.stringify(options.inputData), + format: options.format, + workdir, + }); + + if (runResult.isFailure) { + return Result.fail(new FastReportExecutionError(runResult.error.message, runResult.error)); + } + + return Result.ok(runResult.data); + } + + protected async storageReport(key: ReportStorageKey, payload: Buffer | string): Promise { + try { + await this.reportStorage.write(key, payload); + } catch (error) { + // ⚠️ Importante: no romper generación por fallo de cache + } + } + + protected resolveWorkdir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "fastreport-")); + } + + +protected abstract resolveTemplatePath(): string; + +protected calculateChecksum(filePath: string): string +{ + const buffer = fs.readFileSync(filePath); + return crypto.createHash("sha256").update(buffer).digest("hex"); +} +} + +*/ diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/index.ts b/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts similarity index 66% rename from modules/core/src/api/infrastructure/renderers/fast-report/index.ts rename to modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts index 575cf6a2..266272fe 100644 --- a/modules/core/src/api/infrastructure/renderers/fast-report/index.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/fastreport/index.ts @@ -2,5 +2,4 @@ export * from "./fastreport-errors"; export * from "./fastreport-executable-resolver"; export * from "./fastreport-process-runner"; export * from "./fastreport-render-options.type"; -export * from "./fastreport-renderer.base"; -export * from "./fastreport-template-resolver"; +export * from "./fastreport-renderer"; diff --git a/modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts b/modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts similarity index 99% rename from modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts rename to modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts index 54644706..f80a2765 100644 --- a/modules/core/src/api/infrastructure/renderers/handlebars/handlebars-template-resolver.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/handlebars/handlebars-template-resolver.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs"; import Handlebars from "handlebars"; import { lookup } from "mime-types"; -import { RendererTemplateResolver } from "../renderer-template-resolver"; +import { RendererTemplateResolver } from "../renderer-template-resolver-SOBRA"; export class HandlebarsTemplateResolver extends RendererTemplateResolver { protected readonly hbs = Handlebars.create(); diff --git a/modules/core/src/api/infrastructure/renderers/handlebars/index.ts b/modules/core/src/api/infrastructure/documents/renderers/handlebars/index.ts similarity index 100% rename from modules/core/src/api/infrastructure/renderers/handlebars/index.ts rename to modules/core/src/api/infrastructure/documents/renderers/handlebars/index.ts diff --git a/modules/core/src/api/infrastructure/documents/renderers/index.ts b/modules/core/src/api/infrastructure/documents/renderers/index.ts new file mode 100644 index 00000000..ecdf24e4 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/renderers/index.ts @@ -0,0 +1,3 @@ +export * from "./fastreport"; +export * from "./handlebars"; +export * from "./renderer-template-resolver-SOBRA"; diff --git a/modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts b/modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts similarity index 90% rename from modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts rename to modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts index 077a4461..f8e1b4d6 100644 --- a/modules/core/src/api/infrastructure/renderers/renderer-template-resolver.ts +++ b/modules/core/src/api/infrastructure/documents/renderers/renderer-template-resolver-SOBRA.ts @@ -1,7 +1,9 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import type { IRendererTemplateResolver } from "../../application"; +import type { IRendererTemplateResolver } from "../../../application"; + +import { FastReportTemplateNotFoundError } from "./fastreport"; /** * Resuelve rutas de plantillas para desarrollo y producción. @@ -44,7 +46,7 @@ export abstract class RendererTemplateResolver implements IRendererTemplateResol const filePath = this.resolveAssetPath(dir, templateName); if (!existsSync(filePath)) { - throw new Error( + throw new FastReportTemplateNotFoundError( `Template not found: module=${module} company=${companySlug} name=${templateName}` ); } diff --git a/modules/core/src/api/infrastructure/documents/signing/index.ts b/modules/core/src/api/infrastructure/documents/signing/index.ts new file mode 100644 index 00000000..596bdc19 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/signing/index.ts @@ -0,0 +1 @@ +export * from "./rest-document-signing.service"; diff --git a/modules/core/src/api/infrastructure/documents/signing/rest-document-signing-config.ts b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing-config.ts new file mode 100644 index 00000000..5948c431 --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing-config.ts @@ -0,0 +1,6 @@ +export interface RestDocumentSigningConfig { + readonly signUrl: string; + readonly method?: string; + readonly timeoutMs?: number; + readonly maxRetries?: number; +} diff --git a/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts new file mode 100644 index 00000000..76ca21be --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/signing/rest-document-signing.service.ts @@ -0,0 +1,115 @@ +import type { IDocumentSigningService } from "@erp/core/api/application"; +import type { ICompanyCertificateContext } from "@erp/core/api/application/documents/application-models/company-certificate-context.interface"; + +import type { RestDocumentSigningConfig } from "./rest-document-signing-config"; +import { SimpleCircuitBreaker } from "./simple-circuit-breaker"; + +/** + * Implementación REST del servicio de firma digital de documentos. + * + * - Infra pura + * - Usa retries limitados + * - Usa circuit breaker simple + * - No expone detalles HTTP a Application + */ + +export class RestDocumentSigningService implements IDocumentSigningService { + private readonly signUrl: string; + private readonly method: string; + private readonly timeoutMs: number; + private readonly maxRetries: number; + private readonly circuitBreaker: SimpleCircuitBreaker; + + constructor(config: RestDocumentSigningConfig) { + this.signUrl = config.signUrl; + this.method = config.method ?? "POST"; + this.timeoutMs = config.timeoutMs ?? 10_000; + this.maxRetries = config.maxRetries ?? 2; + + this.circuitBreaker = new SimpleCircuitBreaker(); + } + + async sign(payload: Buffer, context: ICompanyCertificateContext): Promise { + if (!this.circuitBreaker.canExecute()) { + throw new Error("Document signing service unavailable (circuit open)"); + } + + try { + const result = await this.withRetries(() => this.callSigningService(payload, context)); + + this.circuitBreaker.onSuccess(); + return result; + } catch (error) { + this.circuitBreaker.onFailure(); + throw error; + } + } + + private async withRetries(operation: () => Promise): Promise { + let attempt = 0; + + while (true) { + try { + return await operation(); + } catch (error) { + attempt++; + + if (attempt > this.maxRetries || !this.isRetryableError(error)) { + throw error; + } + + await this.delay(200 * attempt); + } + } + } + + private async callSigningService( + payload: Buffer, + context: ICompanyCertificateContext + ): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const response = await fetch(this.signUrl, { + method: this.method, + signal: controller.signal, + body: this.buildFormData(payload, context), + }); + + if (!response.ok) { + const error = new Error(`Signing service responded with status ${response.status}`); + (error as any).status = response.status; + throw error; + } + + return Buffer.from(await response.arrayBuffer()); + } finally { + clearTimeout(timeout); + } + } + + private buildFormData(payload: Buffer, context: ICompanyCertificateContext): FormData { + const form = new FormData(); + + form.append("file", new Blob([new Uint8Array(payload)])); + form.append("certificate_secret_name", context.certificateSecretName); + form.append("certificate_password_secret_name", context.certificatePasswordSecretName); + form.append("company_slug", context.companySlug); + + return form; + } + + private isRetryableError(error: unknown): boolean { + if (error instanceof Error && (error as any).name === "AbortError") { + return true; + } + + const status = (error as any)?.status; + return status === 502 || status === 503 || status === 504; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/modules/core/src/api/infrastructure/documents/signing/simple-circuit-breaker.ts b/modules/core/src/api/infrastructure/documents/signing/simple-circuit-breaker.ts new file mode 100644 index 00000000..46c6bb7e --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/signing/simple-circuit-breaker.ts @@ -0,0 +1,37 @@ +export class SimpleCircuitBreaker { + private failures = 0; + private state: "CLOSED" | "OPEN" = "CLOSED"; + private openedAt = 0; + + constructor( + private readonly failureThreshold = 3, + private readonly resetTimeoutMs = 30_000 + ) {} + + canExecute(): boolean { + if (this.state === "CLOSED") { + return true; + } + + if (Date.now() - this.openedAt > this.resetTimeoutMs) { + this.state = "CLOSED"; + this.failures = 0; + return true; + } + + return false; + } + + onSuccess(): void { + this.failures = 0; + this.state = "CLOSED"; + } + + onFailure(): void { + this.failures++; + if (this.failures >= this.failureThreshold) { + this.state = "OPEN"; + this.openedAt = Date.now(); + } + } +} diff --git a/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts b/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts new file mode 100644 index 00000000..660eff1c --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/storage/filesystem-document-cache-store.ts @@ -0,0 +1,65 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import type { IDocument } from "@erp/core/api/application"; +import type { IDocumentCacheStore } from "@erp/core/api/application/documents/services/document-cache.interface"; + +/** + * Cache técnica de documentos firmados basada en filesystem. + * + * - Best-effort + * - Nunca lanza + * - Cachea SOLO documentos firmados + */ +export class FilesystemDocumentCacheStore implements IDocumentCacheStore { + constructor(private readonly basePath: string) {} + + async get(cacheKey: string): Promise { + try { + const dir = this.resolveDir(cacheKey); + + const payload = await readFile(join(dir, "payload.bin")); + + const meta = JSON.parse(await readFile(join(dir, "meta.json"), "utf-8")); + + return { + payload, + mimeType: meta.mimeType, + filename: meta.filename, + }; + } catch { + // cualquier fallo => cache miss + return null; + } + } + + async set(cacheKey: string, document: IDocument): Promise { + try { + const dir = this.resolveDir(cacheKey); + await mkdir(dir, { recursive: true }); + + await writeFile(join(dir, "payload.bin"), document.payload); + + await writeFile( + join(dir, "meta.json"), + JSON.stringify( + { + mimeType: document.mimeType, + filename: document.filename, + }, + null, + 2 + ) + ); + } catch { + // best-effort: ignorar + } + } + + private resolveDir(cacheKey: string): string { + const hash = createHash("sha256").update(cacheKey).digest("hex"); + + return join(this.basePath, hash); + } +} diff --git a/modules/core/src/api/infrastructure/documents/storage/index.ts b/modules/core/src/api/infrastructure/documents/storage/index.ts new file mode 100644 index 00000000..942c21da --- /dev/null +++ b/modules/core/src/api/infrastructure/documents/storage/index.ts @@ -0,0 +1 @@ +export * from "./filesystem-document-cache-store"; diff --git a/modules/core/src/api/infrastructure/errors/infrastructure-api-contract-error.ts b/modules/core/src/api/infrastructure/errors/infrastructure-api-contract-error.ts new file mode 100644 index 00000000..b2a4eacf --- /dev/null +++ b/modules/core/src/api/infrastructure/errors/infrastructure-api-contract-error.ts @@ -0,0 +1,11 @@ +import { InfrastructureError } from "./infrastructure-errors"; + +export class InfrastructureAPIContractError extends InfrastructureError { + public readonly code = "API_CONTRACT_VIOLATION" as const; + constructor(message = "API contract violation", options?: ErrorOptions) { + super(message, options); + } +} + +export const isInfrastructureAPIContractError = (e: unknown): e is InfrastructureAPIContractError => + e instanceof InfrastructureAPIContractError; diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts index c2a7f2f4..6b7630e4 100644 --- a/modules/core/src/api/infrastructure/express/api-error-mapper.ts +++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts @@ -18,22 +18,22 @@ import { isValidationErrorCollection, } from "@repo/rdx-ddd"; +import { isSchemaError } from "../../../common/schemas"; +import { type DocumentGenerationError, isDocumentGenerationError } from "../../application"; import { type DuplicateEntityError, type EntityNotFoundError, isDuplicateEntityError, isEntityNotFoundError, } from "../../domain"; +import { type FastReportError, isFastReportError } from "../documents"; import { type InfrastructureRepositoryError, type InfrastructureUnavailableError, isInfrastructureRepositoryError, isInfrastructureUnavailableError, } from "../errors"; -import { - type FastReportError, - isFastReportError, -} from "../renderers/fast-report/fastreport-errors"; +import type { InfrastructureAPIContractError } from "../errors/infrastructure-api-contract-error"; import { ApiError, @@ -164,6 +164,18 @@ const defaultRules: ReadonlyArray = [ }, // 5.5) Errores de FastReport inesperados + { + priority: 55, + matches: (e) => isDocumentGenerationError(e), + build: (e) => { + const error = e as DocumentGenerationError; + const title = + error.documentErrorType === "METADATA" + ? "Invalid document render error" + : "Unexcepted document render error"; + return new InternalApiError(error.message, title); + }, + }, { priority: 55, matches: (e) => isFastReportError(e), @@ -193,6 +205,14 @@ const defaultRules: ReadonlyArray = [ matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError", build: (e) => new ForbiddenApiError((e as Error).message || "Forbidden"), }, + + // 8) API contract violation + { + priority: 30, + matches: (e) => isSchemaError(e), + build: (e) => + new InternalApiError((e as InfrastructureAPIContractError).message, "API contract violation"), + }, ]; // Fallback genérico (500) diff --git a/modules/core/src/api/infrastructure/index.ts b/modules/core/src/api/infrastructure/index.ts index 6ab7bade..49b520e5 100644 --- a/modules/core/src/api/infrastructure/index.ts +++ b/modules/core/src/api/infrastructure/index.ts @@ -1,8 +1,9 @@ export * from "./database"; +export * from "./di"; +export * from "./documents"; export * from "./errors"; export * from "./express"; export * from "./logger"; export * from "./mappers"; -export * from "./renderers"; -export * from "./reporting"; export * from "./sequelize"; +export * from "./storage"; diff --git a/modules/core/src/api/infrastructure/mappers/mapper-registry.ts b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts index fd0585d8..289f7817 100644 --- a/modules/core/src/api/infrastructure/mappers/mapper-registry.ts +++ b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts @@ -1,5 +1,6 @@ import { InfrastructureError } from "../errors"; -import { + +import type { IMapperRegistry, MapperDomainKey, MapperKey, diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts deleted file mode 100644 index ec5aad39..00000000 --- a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-renderer.base.ts +++ /dev/null @@ -1,140 +0,0 @@ -import crypto from "node:crypto"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -import { Maybe, Result } from "@repo/rdx-utils"; - -import { Renderer } from "../../../application"; -import type { ReportStorage, ReportStorageKey } from "../../reporting"; - -import { - type FastReportError, - FastReportExecutionError, - FastReportTemplateNotFoundError, -} from "./fastreport-errors"; -import type { FastReportExecutableResolver } from "./fastreport-executable-resolver"; -import type { FastReportProcessRunner } from "./fastreport-process-runner"; -import type { FastReportRenderOptions } from "./fastreport-render-options.type"; -import type { FastReportTemplateResolver } from "./fastreport-template-resolver"; - -export type FastReportRenderOutput = Result< - { payload: Buffer | string; templateChecksum: string }, - FastReportError ->; - -/** - * Clase base para renderers FastReport. - */ -export abstract class FastReportRenderer extends Renderer { - constructor( - protected readonly executableResolver: FastReportExecutableResolver, - protected readonly processRunner: FastReportProcessRunner, - protected readonly templateResolver: FastReportTemplateResolver, - protected readonly reportStorage: ReportStorage - ) { - super(); - } - - /** - * Punto de entrada común para renderizado. - */ - protected async renderInternal( - options: FastReportRenderOptions - ): Promise { - if (process.env.NODE_ENV !== "development") { - // Cache (read-through) - const cached = await this.tryReadFromCache(options.storageKey); - if (cached.isSome()) { - return Result.ok({ - payload: cached.unwrap(), - templateChecksum: "CACHED", - }); - } - } - - try { - // Resolver plantilla - const templatePath = this.resolveTemplatePath(); - console.log("Using FastReport template:", templatePath); - - if (!fs.existsSync(templatePath)) { - return Result.fail(new FastReportTemplateNotFoundError(templatePath)); - } - - const templateChecksum = this.calculateChecksum(templatePath); - - // Llamar a FastReport - const callResult = await this.callFastReportGenerator(options, templatePath); - - if (callResult.isFailure) { - return Result.fail(callResult.error); - } - - // Guardar documento generado (best-effort) - await this.storageReport(options.storageKey, callResult.data); - - return Result.ok({ - payload: callResult.data, - templateChecksum, - }); - } catch (error) { - console.error("FastReport rendering error:", error); - return Result.fail( - new FastReportExecutionError("Unexpected FastReport rendering error", error as Error) - ); - } - } - - protected async tryReadFromCache(docKey: ReportStorageKey): Promise> { - if (await this.reportStorage.exists(docKey)) { - const cached = await this.reportStorage.read(docKey); - return Maybe.some(cached); - } - return Maybe.none(); - } - - protected async callFastReportGenerator( - options: FastReportRenderOptions, - templatePath: string - ): Promise> { - const executablePath = this.executableResolver.resolve(); - const workdir = this.resolveWorkdir(); - - const runResult = await this.processRunner.run(executablePath, { - templatePath, - data: JSON.stringify(options.inputData), - format: options.format, - workdir, - }); - - if (runResult.isFailure) { - return Result.fail(new FastReportExecutionError(runResult.error.message, runResult.error)); - } - - return Result.ok(runResult.data); - } - - protected async storageReport(key: ReportStorageKey, payload: Buffer | string): Promise { - try { - await this.reportStorage.write(key, payload); - } catch (error) { - // ⚠️ Importante: no romper generación por fallo de cache - } - } - - protected resolveWorkdir(): string { - return fs.mkdtempSync(path.join(os.tmpdir(), "fastreport-")); - } - - /** - * Cada renderer concreto decide - * qué plantilla usar. - */ - protected abstract resolveTemplatePath(): string; - - protected calculateChecksum(filePath: string): string { - const buffer = fs.readFileSync(filePath); - return crypto.createHash("sha256").update(buffer).digest("hex"); - } -} diff --git a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-template-resolver.ts b/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-template-resolver.ts deleted file mode 100644 index 68a339b3..00000000 --- a/modules/core/src/api/infrastructure/renderers/fast-report/fastreport-template-resolver.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { RendererTemplateResolver } from "../renderer-template-resolver"; - -export class FastReportTemplateResolver extends RendererTemplateResolver {} diff --git a/modules/core/src/api/infrastructure/renderers/index.ts b/modules/core/src/api/infrastructure/renderers/index.ts deleted file mode 100644 index 433083fb..00000000 --- a/modules/core/src/api/infrastructure/renderers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./fast-report"; -export * from "./handlebars"; -export * from "./renderer-template-resolver"; diff --git a/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts b/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts deleted file mode 100644 index 0760b5b9..00000000 --- a/modules/core/src/api/infrastructure/reporting/filesystem-report-storage.ts +++ /dev/null @@ -1,40 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; - -import { buildSafePath } from "@repo/rdx-utils"; - -import type { ReportStorage, ReportStorageKey } from "./report-storage"; - -export class FileSystemReportStorage implements ReportStorage { - public constructor(private readonly basePath: string) {} - - public async exists(docKey: ReportStorageKey): Promise { - try { - await fs.access(this.resolvePath(docKey)); - return true; - } catch { - return false; - } - } - - public async read(docKey: ReportStorageKey): Promise { - const filePath = this.resolvePath(docKey); - return docKey.format === "PDF" ? fs.readFile(filePath) : fs.readFile(filePath, "utf-8"); - } - - public async write(docKey: ReportStorageKey, payload: Buffer | string): Promise { - const filePath = this.resolvePath(docKey); - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, payload); - } - - private resolvePath(key: ReportStorageKey): string { - const ext = key.format.toLowerCase(); - - return buildSafePath({ - basePath: this.basePath, - segments: [key.documentType.toLowerCase()], - filename: `${key.documentId}.${ext}`, - }); - } -} diff --git a/modules/core/src/api/infrastructure/reporting/index.ts b/modules/core/src/api/infrastructure/reporting/index.ts deleted file mode 100644 index b90fe6c7..00000000 --- a/modules/core/src/api/infrastructure/reporting/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./filesystem-report-storage"; -export * from "./report-storage"; diff --git a/modules/core/src/api/infrastructure/reporting/report-storage.ts b/modules/core/src/api/infrastructure/reporting/report-storage.ts deleted file mode 100644 index 197f86f5..00000000 --- a/modules/core/src/api/infrastructure/reporting/report-storage.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type ReportStorageKey = { - documentType: string; - documentId: string; - format: "PDF" | "HTML"; -}; - -export interface ReportStorage { - exists(docKey: ReportStorageKey): Promise; - read(docKey: ReportStorageKey): Promise; - write(docKey: ReportStorageKey, payload: Buffer | string): Promise; -} diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts index 9d5a9f94..a004b629 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts @@ -1,8 +1,9 @@ -import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; -import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; +import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; +import type { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; -import { IMapperRegistry } from "../mappers"; +import type { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; + +import type { IMapperRegistry } from "../mappers"; export abstract class SequelizeRepository implements IAggregateRootRepository { protected readonly _database!: Sequelize; diff --git a/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts b/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts new file mode 100644 index 00000000..80758ca6 --- /dev/null +++ b/modules/core/src/api/infrastructure/storage/filesystem-signed-document-storage.ts @@ -0,0 +1,50 @@ +import { createHash } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import type { IDocument, IDocumentStorage } from "../../application"; + +/** + * Persistencia best-effort de documentos basada en filesystem. + * + * - Infra pura + * - Nunca lanza + * - No afecta al flujo del caso de uso + */ +export class FilesystemDocumentStorage implements IDocumentStorage { + public constructor(private readonly basePath: string) {} + + async save(document: IDocument, metadata: Record): Promise { + try { + const dir = this.resolveDir(metadata); + await mkdir(dir, { recursive: true }); + + await writeFile(path.join(dir, "document.bin"), document.payload); + + await writeFile( + path.join(dir, "document.meta.json"), + JSON.stringify( + { + mimeType: document.mimeType, + filename: document.filename, + metadata, + }, + null, + 2 + ) + ); + } catch { + // best-effort: ignorar errores + } + } + + private resolveDir(metadata: Record): string { + /** + * El storage NO decide claves semánticas. + * Se limita a generar un path técnico estable. + */ + const hash = createHash("sha256").update(JSON.stringify(metadata)).digest("hex"); + + return path.join(this.basePath, hash); + } +} diff --git a/modules/core/src/api/infrastructure/storage/index.ts b/modules/core/src/api/infrastructure/storage/index.ts new file mode 100644 index 00000000..ababca9b --- /dev/null +++ b/modules/core/src/api/infrastructure/storage/index.ts @@ -0,0 +1 @@ +export * from "./filesystem-signed-document-storage"; diff --git a/modules/core/src/common/dto/list-view.response.dto.ts b/modules/core/src/common/dto/list-view.response.dto.ts index 75572f7b..e0c1db11 100644 --- a/modules/core/src/common/dto/list-view.response.dto.ts +++ b/modules/core/src/common/dto/list-view.response.dto.ts @@ -9,7 +9,7 @@ import { MetadataSchema } from "./metadata.dto"; * @returns Zod schema para ListViewDTO */ export const PaginationSchema = z.object({ - page: z.number().int().min(1, "Page must be a positive integer"), + page: z.number().int().min(0, "Page must be a positive integer"), per_page: z.number().int().min(1, "Items per page must be a positive integer"), total_pages: z.number().int().min(0, "Total pages must be a non-negative integer"), total_items: z.number().int().min(0, "Total items must be a non-negative integer"), diff --git a/modules/core/src/common/schemas/index.ts b/modules/core/src/common/schemas/index.ts index e0906790..e5649dba 100644 --- a/modules/core/src/common/schemas/index.ts +++ b/modules/core/src/common/schemas/index.ts @@ -1 +1,2 @@ export * from "./core.schemas"; +export * from "./schema-error"; diff --git a/modules/core/src/common/schemas/schema-error.ts b/modules/core/src/common/schemas/schema-error.ts new file mode 100644 index 00000000..46d4961c --- /dev/null +++ b/modules/core/src/common/schemas/schema-error.ts @@ -0,0 +1,3 @@ +import { ZodError } from "zod"; + +export const isSchemaError = (e: unknown): e is ZodError => e instanceof ZodError; diff --git a/modules/customer-invoices/src/api/application/index.ts b/modules/customer-invoices/src/api/application/index.ts index 76373b44..ffdca24a 100644 --- a/modules/customer-invoices/src/api/application/index.ts +++ b/modules/customer-invoices/src/api/application/index.ts @@ -1,3 +1,5 @@ -export * from "./presenters"; -export * from "./services"; +//export * from "./services"; +//export * from "./snapshot-builders"; + +export * from "./issued-invoices"; export * from "./use-cases"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts new file mode 100644 index 00000000..02b05bb8 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts @@ -0,0 +1,3 @@ +export * from "./issued-invoice-document.model"; +export * from "./report-cache-key"; +export * from "./snapshots"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts new file mode 100644 index 00000000..fa02ca33 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts @@ -0,0 +1,33 @@ +/** + * Documento legal generado para una factura emitida. + * + */ + +export interface IIssuedInvoiceDocument { + payload: Buffer; + mimeType: string; + filename: string; +} + +export class IssuedInvoiceDocument implements IIssuedInvoiceDocument { + public readonly payload: Buffer; + public readonly mimeType: string; + public readonly filename: string; + + constructor(params: { + payload: Buffer; + filename: string; + }) { + if (!params.payload || params.payload.length === 0) { + throw new Error("IssuedInvoiceDocument payload cannot be empty"); + } + + if (!params.filename.toLowerCase().endsWith(".pdf")) { + throw new Error("IssuedInvoiceDocument filename must end with .pdf"); + } + + this.payload = params.payload; + this.mimeType = "application/pdf"; + this.filename = params.filename; + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts new file mode 100644 index 00000000..a8f96f1d --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts @@ -0,0 +1,59 @@ +import type { UniqueID } from "@repo/rdx-ddd"; + +/** + * Clave determinista que identifica de forma única + * un documento legal generado. + * + * Encapsula la regla de idempotencia del caso de uso. + */ +export class ReportCacheKey { + private readonly value: string; + + private constructor(value: string) { + this.value = value; + } + + static forIssuedInvoice(params: { + companyId: UniqueID; + invoiceId: UniqueID; + language: string; + canonicalModelHash: string; + templateChecksum: string; + rendererVersion: string; + signingProviderVersion: string; + }): ReportCacheKey { + const { + companyId, + invoiceId, + language, + canonicalModelHash, + templateChecksum, + rendererVersion, + signingProviderVersion, + } = params; + + // Fail-fast: campos obligatorios + for (const [key, value] of Object.entries(params)) { + if (!value || String(value).trim() === "") { + throw new Error(`ReportCacheKey missing field: ${key}`); + } + } + + return new ReportCacheKey( + [ + "issued-invoice", + companyId, + invoiceId, + language, + canonicalModelHash, + templateChecksum, + rendererVersion, + signingProviderVersion, + ].join("__") + ); + } + + toString(): string { + return this.value; + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts new file mode 100644 index 00000000..0b54621c --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts @@ -0,0 +1,4 @@ +export * from "./issued-invoice-full-snapshot"; +export * from "./issued-invoice-item-full-snapshot"; +export * from "./issued-invoice-recipient-full-snapshot"; +export * from "./issued-invoice-verifactu-full-snapshot"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts new file mode 100644 index 00000000..856474dd --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts @@ -0,0 +1,69 @@ +import type { IssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot"; +import type { IssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot"; +import type { IssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot"; + +export interface IssuedInvoiceFullSnapshot { + id: string; + company_id: string; + + is_proforma: "true" | "false"; + invoice_number: string; + status: string; + series: string; + + invoice_date: string; + operation_date: string; + + reference: string; + description: string; + notes: string; + + language_code: string; + currency_code: string; + + customer_id: string; + recipient: IssuedInvoiceRecipientFullSnapshot; + + payment_method?: { + payment_id: string; + payment_description: string; + }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + items_discount_amount: { value: string; scale: string; currency_code: string }; + + discount_percentage: { value: string; scale: string }; + discount_amount: { value: string; scale: string; currency_code: string }; + + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_amount: { value: string; scale: string; currency_code: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + total_amount: { value: string; scale: string; currency_code: string }; + + taxes: Array<{ + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_code: string; + iva_percentage: { value: string; scale: string }; + iva_amount: { value: string; scale: string; currency_code: string }; + + rec_code: string; + rec_percentage: { value: string; scale: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + + retention_code: string; + retention_percentage: { value: string; scale: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + }>; + + verifactu: IssuedInvoiceVerifactuFullSnapshot; + items: IssuedInvoiceItemFullSnapshot[]; + + metadata?: Record; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-item-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-item-full-snapshot.ts new file mode 100644 index 00000000..f8240fe1 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-item-full-snapshot.ts @@ -0,0 +1,34 @@ +export interface IssuedInvoiceItemFullSnapshot { + id: string; + is_valued: string; + position: string; + description: string; + + quantity: { value: string; scale: string }; + unit_amount: { value: string; scale: string; currency_code: string }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + + discount_percentage: { value: string; scale: string }; + discount_amount: { value: string; scale: string; currency_code: string }; + + global_discount_percentage: { value: string; scale: string }; + global_discount_amount: { value: string; scale: string; currency_code: string }; + + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_code: string; + iva_percentage: { value: string; scale: string }; + iva_amount: { value: string; scale: string; currency_code: string }; + + rec_code: string; + rec_percentage: { value: string; scale: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + + retention_code: string; + retention_percentage: { value: string; scale: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + total_amount: { value: string; scale: string; currency_code: string }; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-recipient-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-recipient-full-snapshot.ts new file mode 100644 index 00000000..8c6c5d9d --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-recipient-full-snapshot.ts @@ -0,0 +1,11 @@ +export interface IssuedInvoiceRecipientFullSnapshot { + id: string; + name: string; + tin: string; + street: string; + street2: string; + city: string; + province: string; + postal_code: string; + country: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-verifactu-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-verifactu-full-snapshot.ts new file mode 100644 index 00000000..cf656ff3 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-verifactu-full-snapshot.ts @@ -0,0 +1,6 @@ +export interface IssuedInvoiceVerifactuFullSnapshot { + id: string; + status: string; + url: string; + qr_code: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/index.ts new file mode 100644 index 00000000..188f634f --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/index.ts @@ -0,0 +1,3 @@ +export * from "./full"; +export * from "./list"; +export * from "./report"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts new file mode 100644 index 00000000..977ca20e --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-list-item-snapshot"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/issued-invoice-list-item-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/issued-invoice-list-item-snapshot.ts new file mode 100644 index 00000000..c38d50fa --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/issued-invoice-list-item-snapshot.ts @@ -0,0 +1,46 @@ +export interface IssuedInvoiceListItemSnapshot { + id: string; + company_id: string; + is_proforma: boolean; + + customer_id: string; + + invoice_number: string; + status: string; + series: string; + + invoice_date: string; + operation_date: string; + + language_code: string; + currency_code: string; + + reference: string; + description: string; + + recipient: { + tin: string; + name: string; + street: string; + street2: string; + city: string; + postal_code: string; + province: string; + country: string; + }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + discount_percentage: { value: string; scale: string }; + discount_amount: { value: string; scale: string; currency_code: string }; + taxable_amount: { value: string; scale: string; currency_code: string }; + taxes_amount: { value: string; scale: string; currency_code: string }; + total_amount: { value: string; scale: string; currency_code: string }; + + verifactu: { + status: string; + url: string; + qr_code: string; + }; + + metadata?: Record; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts new file mode 100644 index 00000000..536472e8 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts @@ -0,0 +1,3 @@ +export * from "./issued-invoice-report-item-snapshot"; +export * from "./issued-invoice-report-snapshot"; +export * from "./issued-invoice-report-tax-snapshot"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-item-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-item-snapshot.ts new file mode 100644 index 00000000..3cf6f161 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-item-snapshot.ts @@ -0,0 +1,11 @@ +export interface IssuedInvoiceReportItemSnapshot { + description: string; + quantity: string; + unit_amount: string; + subtotal_amount: string; + discount_percentage: string; + discount_amount: string; + taxable_amount: string; + taxes_amount: string; + total_amount: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts new file mode 100644 index 00000000..4cead399 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts @@ -0,0 +1,38 @@ +import type { IssuedInvoiceReportItemSnapshot } from "./issued-invoice-report-item-snapshot"; +import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax-snapshot"; + +export interface IssuedInvoiceReportSnapshot { + id: string; + company_id: string; + invoice_number: string; + series: string; + status: string; + + language_code: string; + currency_code: string; + + invoice_date: string; + payment_method: string; + + recipient: { + name: string; + tin: string; + format_address: string; + }; + + items: IssuedInvoiceReportItemSnapshot[]; + taxes: IssuedInvoiceReportTaxSnapshot[]; + + subtotal_amount: string; + discount_percentage: string; + discount_amount: string; + taxable_amount: string; + taxes_amount: string; + total_amount: string; + + verifactu: { + status: string; + url: string; + qr_code: string; + }; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-tax-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-tax-snapshot.ts new file mode 100644 index 00000000..2c1094a1 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-tax-snapshot.ts @@ -0,0 +1,17 @@ +export interface IssuedInvoiceReportTaxSnapshot { + taxable_amount: string; + + iva_code: string; + iva_percentage: string; + iva_amount: string; + + rec_code: string; + rec_percentage: string; + rec_amount: string; + + retention_code: string; + retention_percentage: string; + retention_amount: string; + + taxes_amount: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/documents.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/documents.di.ts new file mode 100644 index 00000000..7dfcaf05 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/documents.di.ts @@ -0,0 +1,25 @@ +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); +}; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/finder.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/finder.di.ts new file mode 100644 index 00000000..498d3092 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/finder.di.ts @@ -0,0 +1,8 @@ +import type { ICustomerInvoiceRepository } from "../../../domain"; +import { type IIssuedInvoiceFinder, IssuedInvoiceFinder } from "../services/issued-invoice-finder"; + +export function buildIssuedInvoiceFinder( + repository: ICustomerInvoiceRepository +): IIssuedInvoiceFinder { + return new IssuedInvoiceFinder(repository); +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts new file mode 100644 index 00000000..cccc4c1c --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts @@ -0,0 +1,4 @@ +export * from "./documents.di"; +export * from "./finder.di"; +export * from "./snapshot-builders.di"; +export * from "./use-cases.di"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/snapshot-builders.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/snapshot-builders.di.ts new file mode 100644 index 00000000..d7547abe --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/snapshot-builders.di.ts @@ -0,0 +1,43 @@ +// application/issued-invoices/di/snapshot-builders.di.ts + +import { IssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders"; +import { + IssuedInvoiceFullSnapshotBuilder, + IssuedInvoiceItemsFullSnapshotBuilder, + IssuedInvoiceRecipientFullSnapshotBuilder, + IssuedInvoiceVerifactuFullSnapshotBuilder, +} from "../snapshot-builders/full"; +import { + IssuedInvoiceItemReportSnapshotBuilder, + IssuedInvoiceReportSnapshotBuilder, + IssuedInvoiceTaxReportSnapshotBuilder, +} from "../snapshot-builders/report"; + +export function buildIssuedInvoiceSnapshotBuilders() { + const itemsBuilder = new IssuedInvoiceItemsFullSnapshotBuilder(); + + const recipientBuilder = new IssuedInvoiceRecipientFullSnapshotBuilder(); + + const verifactuBuilder = new IssuedInvoiceVerifactuFullSnapshotBuilder(); + + const fullSnapshotBuilder = new IssuedInvoiceFullSnapshotBuilder( + itemsBuilder, + recipientBuilder, + verifactuBuilder + ); + + const listSnapshotBuilder = new IssuedInvoiceListItemSnapshotBuilder(); + + const itemsReportBuilder = new IssuedInvoiceItemReportSnapshotBuilder(); + const taxesReportBuilder = new IssuedInvoiceTaxReportSnapshotBuilder(); + const reportSnapshotBuilder = new IssuedInvoiceReportSnapshotBuilder( + itemsReportBuilder, + taxesReportBuilder + ); + + return { + full: fullSnapshotBuilder, + list: listSnapshotBuilder, + report: reportSnapshotBuilder, + }; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/use-cases.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/use-cases.di.ts new file mode 100644 index 00000000..e474c530 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/use-cases.di.ts @@ -0,0 +1,51 @@ +import type { ITransactionManager } from "@erp/core/api"; + +import type { IIssuedInvoiceDocumentReportService, IIssuedInvoiceFinder } from "../services"; +import type { IIssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders"; +import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders/full"; +import type { IIssuedInvoiceReportSnapshotBuilder } from "../snapshot-builders/report"; +import { + GetIssuedInvoiceByIdUseCase, + ListIssuedInvoicesUseCase, + ReportIssuedInvoiceUseCase, +} from "../use-cases"; + +export function buildGetIssuedInvoiceByIdUseCase(deps: { + finder: IIssuedInvoiceFinder; + fullSnapshotBuilder: IIssuedInvoiceFullSnapshotBuilder; + transactionManager: ITransactionManager; +}) { + return new GetIssuedInvoiceByIdUseCase( + deps.finder, + deps.fullSnapshotBuilder, + deps.transactionManager + ); +} + +export function buildListIssuedInvoicesUseCase(deps: { + finder: IIssuedInvoiceFinder; + itemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder; + transactionManager: ITransactionManager; +}) { + return new ListIssuedInvoicesUseCase( + deps.finder, + deps.itemSnapshotBuilder, + deps.transactionManager + ); +} + +export function buildReportIssuedInvoiceUseCase(deps: { + finder: IIssuedInvoiceFinder; + fullSnapshotBuilder: IIssuedInvoiceFullSnapshotBuilder; + reportSnapshotBuilder: IIssuedInvoiceReportSnapshotBuilder; + documentService: IIssuedInvoiceDocumentReportService; + transactionManager: ITransactionManager; +}) { + return new ReportIssuedInvoiceUseCase( + deps.finder, + deps.fullSnapshotBuilder, + deps.reportSnapshotBuilder, + deps.documentService, + deps.transactionManager + ); +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts new file mode 100644 index 00000000..bca58664 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts @@ -0,0 +1 @@ +export * from "./sign-document-command"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts b/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts new file mode 100644 index 00000000..c5029ef0 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts @@ -0,0 +1,15 @@ +/** + * DTO de Application para solicitar la firma de un documento. + * + * Se utiliza exclusivamente para intercambiar datos con la capa + * de infraestructura (DocumentSigningService). + * + * No contiene lógica ni validaciones de negocio. + */ +export interface SignDocumentCommand { + /** PDF sin firmar */ + readonly file: Buffer; + + /** Identificador estable de la empresa */ + readonly companyId: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/index.ts new file mode 100644 index 00000000..1a960d9a --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/index.ts @@ -0,0 +1,5 @@ +export * from "./application-models"; +export * from "./di"; +export * from "./services"; +export * from "./snapshot-builders"; +export * from "./use-cases"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts new file mode 100644 index 00000000..e97584f3 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts @@ -0,0 +1,5 @@ +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"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts new file mode 100644 index 00000000..8f08c463 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts @@ -0,0 +1,6 @@ +import type { DocumentGenerationService } from "@erp/core/api"; + +import type { IssuedInvoiceReportSnapshot } from "../application-models"; + +export interface IssuedInvoiceDocumentGeneratorService + extends DocumentGenerationService {} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts new file mode 100644 index 00000000..eefe2530 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts @@ -0,0 +1,44 @@ +import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; + +import type { IssuedInvoiceReportSnapshot } from "../application-models"; + +/** + * Construye los metadatos del documento PDF de una factura emitida. + * + * - Application-level + * - Determinista + * - Sin IO + */ +export class IssuedInvoiceDocumentMetadataFactory + implements IDocumentMetadataFactory +{ + build(snapshot: IssuedInvoiceReportSnapshot): IDocumentMetadata { + if (!snapshot.id) { + throw new Error("IssuedInvoiceReportSnapshot.invoiceId is required"); + } + + if (!snapshot.company_id) { + throw new Error("IssuedInvoiceReportSnapshot.companyId is required"); + } + + return { + documentType: "issued-invoice", + documentId: snapshot.id, + companyId: snapshot.company_id, + format: "PDF", + languageCode: snapshot.language_code ?? "es", + filename: this.buildFilename(snapshot), + cacheKey: this.buildCacheKey(snapshot), + }; + } + + private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string { + // Ejemplo: factura-F2024-000123.pdf + return `factura-${snapshot.invoice_number}.pdf`; + } + + private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string { + // Versionado explícito para invalidaciones futuras + return ["issued-invoice", snapshot.company_id, snapshot.invoice_number, "v1"].join(":"); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts new file mode 100644 index 00000000..7c55d363 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts @@ -0,0 +1,10 @@ +import type { Result } from "@repo/rdx-utils"; + +import type { IIssuedInvoiceDocument, IssuedInvoiceReportSnapshot } from "../application-models"; + +export interface IIssuedInvoiceDocumentRenderer { + render(input: { + snapshot: IssuedInvoiceReportSnapshot; + documentId: string; + }): Promise>; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts new file mode 100644 index 00000000..37b515f6 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts @@ -0,0 +1,88 @@ +import type { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Collection, Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import type { CustomerInvoice, ICustomerInvoiceRepository } from "../../../domain"; + +export interface IIssuedInvoiceFinder { + findIssuedInvoiceById( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise>; + + issuedInvoiceExists( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise>; + + findIssuedInvoicesByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>>; +} + +export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { + constructor(private readonly repository: ICustomerInvoiceRepository) {} + + async findIssuedInvoiceById( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.getIssuedInvoiceByIdInCompany(companyId, invoiceId, transaction, {}); + } + + async findProformaById( + companyId: UniqueID, + proformaId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); + } + + async issuedInvoiceExists( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, { + is_proforma: false, + }); + } + + async proformaExists( + companyId: UniqueID, + proformaId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { + is_proforma: true, + }); + } + + async findIssuedInvoicesByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + return this.repository.findIssuedInvoicesByCriteriaInCompany( + companyId, + criteria, + transaction, + {} + ); + } + + async findProformasByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {}); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts new file mode 100644 index 00000000..e1723728 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts @@ -0,0 +1,31 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { + CustomerInvoiceNumber, + CustomerInvoiceSerie, + ICustomerInvoiceNumberGenerator, +} from "../../../domain"; + +export interface IIssuedInvoiceNumberService { + /** + * Devuelve el siguiente número disponible para una factura emitida. + */ + nextIssuedInvoiceNumber( + companyId: UniqueID, + series: Maybe, + transaction: unknown + ): Promise>; +} + +export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService { + constructor(private readonly numberGenerator: ICustomerInvoiceNumberGenerator) {} + + async nextIssuedInvoiceNumber( + companyId: UniqueID, + series: Maybe, + transaction: unknown + ): Promise> { + return this.numberGenerator.nextForCompany(companyId, series, transaction); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-report-snapshot-builder.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts new file mode 100644 index 00000000..dabd8f02 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts @@ -0,0 +1,32 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import type { CustomerInvoice } from "../../../domain/aggregates"; +import type { ICustomerInvoiceRepository } from "../../../domain/repositories"; + +export type IIssuedInvoiceWriteService = {}; + +export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService { + constructor(private readonly repository: ICustomerInvoiceRepository) {} + + /** + * Emite (crea) una factura definitiva a partir de una factura ya preparada. + * + * Asume que: + * - el número ya ha sido asignado + * - el estado es correcto + */ + async createIssuedInvoice( + companyId: UniqueID, + invoice: CustomerInvoice, + transaction: Transaction + ): Promise> { + const result = await this.repository.create(invoice, transaction); + if (result.isFailure) { + return Result.fail(result.error); + } + + return result; + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts new file mode 100644 index 00000000..c0dd789a --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts @@ -0,0 +1,4 @@ +export * from "./issued-invoice-full-snapshot-builder"; +export * from "./issued-invoice-items-full-snapshot-builder"; +export * from "./issued-invoice-recipient-full-snapshot-builder"; +export * from "./issued-invoice-verifactu-full-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts similarity index 73% rename from modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts index 777c97e2..48f6393b 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts @@ -1,36 +1,28 @@ -import { Presenter } from "@erp/core/api"; +import type { ISnapshotBuilder } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; +import type { IssuedInvoiceFullSnapshot } from "../../application-models"; -import type { IssuedInvoiceItemsFullPresenter } from "./issued-invoice-items.full.presenter"; -import type { IssuedInvoiceRecipientFullPresenter } from "./issued-invoice-recipient.full.presenter"; -import type { IssuedInvoiceVerifactuFullPresenter } from "./issued-invoice-verifactu.full.presenter"; +import type { IIssuedInvoiceItemsFullSnapshotBuilder } from "./issued-invoice-items-full-snapshot-builder"; +import type { IIssuedInvoiceRecipientFullSnapshotBuilder } from "./issued-invoice-recipient-full-snapshot-builder"; +import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder"; -export class IssuedInvoiceFullPresenter extends Presenter< - CustomerInvoice, - GetIssuedInvoiceByIdResponseDTO -> { - toOutput(invoice: CustomerInvoice): GetIssuedInvoiceByIdResponseDTO { - const itemsPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice-items", - projection: "FULL", - }) as IssuedInvoiceItemsFullPresenter; +export interface IIssuedInvoiceFullSnapshotBuilder + extends ISnapshotBuilder {} - const recipientPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice-recipient", - projection: "FULL", - }) as IssuedInvoiceRecipientFullPresenter; +export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnapshotBuilder { + constructor( + private readonly itemsBuilder: IIssuedInvoiceItemsFullSnapshotBuilder, + private readonly recipientBuilder: IIssuedInvoiceRecipientFullSnapshotBuilder, + private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder + ) {} - const verifactuPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice-verifactu", - projection: "FULL", - }) as IssuedInvoiceVerifactuFullPresenter; + toOutput(invoice: CustomerInvoice): IssuedInvoiceFullSnapshot { + const items = this.itemsBuilder.toOutput(invoice.items); + const recipient = this.recipientBuilder.toOutput(invoice); + const verifactu = this.verifactuBuilder.toOutput(invoice); - const recipient = recipientPresenter.toOutput(invoice); - const items = itemsPresenter.toOutput(invoice.items); - const verifactu = verifactuPresenter.toOutput(invoice); const allAmounts = invoice.calculateAllAmounts(); const payment = invoice.paymentMethod.match( diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts similarity index 79% rename from modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts index 3bba23e8..e8985fac 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts @@ -1,19 +1,16 @@ -import { Presenter } from "@erp/core/api"; -import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; +import type { ISnapshotBuilder } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; -import type { ArrayElement } from "@repo/rdx-utils"; import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; +import type { IssuedInvoiceItemFullSnapshot } from "../../application-models"; -type GetIssuedInvoiceItemByInvoiceIdResponseDTO = ArrayElement< - GetIssuedInvoiceByIdResponseDTO["items"] ->; +export interface IIssuedInvoiceItemsFullSnapshotBuilder + extends ISnapshotBuilder {} -export class IssuedInvoiceItemsFullPresenter extends Presenter { - private _mapItem( - invoiceItem: CustomerInvoiceItem, - index: number - ): GetIssuedInvoiceItemByInvoiceIdResponseDTO { +export class IssuedInvoiceItemsFullSnapshotBuilder + implements IIssuedInvoiceItemsFullSnapshotBuilder +{ + private mapItem(invoiceItem: CustomerInvoiceItem, index: number): IssuedInvoiceItemFullSnapshot { const allAmounts = invoiceItem.calculateAllAmounts(); return { @@ -92,7 +89,7 @@ export class IssuedInvoiceItemsFullPresenter extends Presenter { }; } - toOutput(invoiceItems: CustomerInvoiceItems): GetIssuedInvoiceByIdResponseDTO["items"] { - return invoiceItems.map(this._mapItem); + toOutput(invoiceItems: CustomerInvoiceItems): IssuedInvoiceItemFullSnapshot[] { + return invoiceItems.map((item, index) => this.mapItem(item, index)); } } diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts new file mode 100644 index 00000000..d40faebf --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts @@ -0,0 +1,45 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; + +import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; +import type { IssuedInvoiceRecipientFullSnapshot } from "../../application-models"; + +export interface IIssuedInvoiceRecipientFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceRecipientFullSnapshotBuilder + implements IIssuedInvoiceRecipientFullSnapshotBuilder +{ + toOutput(invoice: CustomerInvoice): IssuedInvoiceRecipientFullSnapshot { + if (!invoice.recipient) { + throw DomainValidationError.requiredValue("recipient", { + cause: invoice, + }); + } + + return invoice.recipient.match( + (recipient: InvoiceRecipient) => ({ + id: invoice.customerId.toString(), + name: recipient.name.toString(), + tin: recipient.tin.toString(), + street: toEmptyString(recipient.street, (v) => v.toString()), + street2: toEmptyString(recipient.street2, (v) => v.toString()), + city: toEmptyString(recipient.city, (v) => v.toString()), + province: toEmptyString(recipient.province, (v) => v.toString()), + postal_code: toEmptyString(recipient.postalCode, (v) => v.toString()), + country: toEmptyString(recipient.country, (v) => v.toString()), + }), + () => ({ + id: "", + name: "", + tin: "", + street: "", + street2: "", + city: "", + province: "", + postal_code: "", + country: "", + }) + ); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts similarity index 52% rename from modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts index f0cd37d3..9885c067 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-verifactu.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts @@ -1,13 +1,16 @@ -import { Presenter } from "@erp/core/api"; +import type { ISnapshotBuilder } from "@erp/core/api"; import { DomainValidationError } from "@repo/rdx-ddd"; -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; import type { CustomerInvoice } from "../../../../domain"; +import type { IssuedInvoiceVerifactuFullSnapshot } from "../../application-models"; -type GetIssuedInvoiceVerifactuByIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["verifactu"]; +export interface IIssuedInvoiceVerifactuFullSnapshotBuilder + extends ISnapshotBuilder {} -export class IssuedInvoiceVerifactuFullPresenter extends Presenter { - toOutput(invoice: CustomerInvoice): GetIssuedInvoiceVerifactuByIdResponseDTO { +export class IssuedInvoiceVerifactuFullSnapshotBuilder + implements IIssuedInvoiceVerifactuFullSnapshotBuilder +{ + toOutput(invoice: CustomerInvoice): IssuedInvoiceVerifactuFullSnapshot { if (!invoice.verifactu) { throw DomainValidationError.requiredValue("verifactu", { cause: invoice, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts new file mode 100644 index 00000000..c2be1212 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts @@ -0,0 +1,2 @@ +export * from "./full"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts new file mode 100644 index 00000000..8828ec69 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-list-item-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts new file mode 100644 index 00000000..0cd9e184 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts @@ -0,0 +1,58 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; + +import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; +import type { IssuedInvoiceListItemSnapshot } from "../../application-models/snapshots/list"; + +export interface IIssuedInvoiceListItemSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder { + toOutput(invoice: CustomerInvoiceListDTO): IssuedInvoiceListItemSnapshot { + const recipient = invoice.recipient.toObjectString(); + + const verifactu = invoice.verifactu.match( + (v) => v.toObjectString(), + () => ({ + status: "", + url: "", + qr_code: "", + }) + ); + + return { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + is_proforma: invoice.isProforma, + + customer_id: invoice.customerId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (v) => v.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (v) => v.toDateString()), + reference: toEmptyString(invoice.reference, (v) => v.toString()), + description: toEmptyString(invoice.description, (v) => v.toString()), + + recipient, + + language_code: invoice.languageCode.code, + currency_code: invoice.currencyCode.code, + + subtotal_amount: invoice.subtotalAmount.toObjectString(), + discount_percentage: invoice.discountPercentage.toObjectString(), + discount_amount: invoice.discountAmount.toObjectString(), + taxable_amount: invoice.taxableAmount.toObjectString(), + taxes_amount: invoice.taxesAmount.toObjectString(), + total_amount: invoice.totalAmount.toObjectString(), + + verifactu, + + metadata: { + entity: "issued-invoice", + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts new file mode 100644 index 00000000..fe88cbc8 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts @@ -0,0 +1,3 @@ +export * from "./issued-invoice-items-report-snapshot-builder"; +export * from "./issued-invoice-report-snapshot-builder"; +export * from "./issued-invoice-tax-report-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-items-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-items-report-snapshot-builder.ts new file mode 100644 index 00000000..a3048d8f --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-items-report-snapshot-builder.ts @@ -0,0 +1,40 @@ +import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { + IssuedInvoiceFullSnapshot, + IssuedInvoiceReportItemSnapshot, +} from "../../application-models"; + +export interface IIssuedInvoiceItemReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceItemReportSnapshotBuilder + implements IIssuedInvoiceItemReportSnapshotBuilder +{ + toOutput( + items: IssuedInvoiceFullSnapshot["items"], + params?: ISnapshotBuilderParams + ): IssuedInvoiceReportItemSnapshot[] { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return items.map((item) => ({ + description: item.description, + quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }), + unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions), + subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions), + discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, { + minimumFractionDigits: 0, + }), + discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions), + taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions), + total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions), + })); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot-builder.ts new file mode 100644 index 00000000..e6c5f473 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot-builder.ts @@ -0,0 +1,94 @@ +import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { + IssuedInvoiceFullSnapshot, + IssuedInvoiceReportItemSnapshot, + IssuedInvoiceReportSnapshot, + IssuedInvoiceReportTaxSnapshot, +} from "../../application-models"; + +export interface IIssuedInvoiceReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceReportSnapshotBuilder implements IIssuedInvoiceReportSnapshotBuilder { + constructor( + private readonly itemsBuilder: ISnapshotBuilder< + IssuedInvoiceFullSnapshot["items"], + IssuedInvoiceReportItemSnapshot[] + >, + private readonly taxesBuilder: ISnapshotBuilder< + IssuedInvoiceFullSnapshot["taxes"], + IssuedInvoiceReportTaxSnapshot[] + > + ) {} + + toOutput( + snapshot: IssuedInvoiceFullSnapshot, + params?: ISnapshotBuilderParams + ): IssuedInvoiceReportSnapshot { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return { + id: snapshot.id, + company_id: snapshot.company_id, + invoice_number: snapshot.invoice_number, + series: snapshot.series, + status: snapshot.status, + + language_code: snapshot.language_code, + currency_code: snapshot.currency_code, + + invoice_date: DateHelper.format(snapshot.invoice_date, locale), + + payment_method: snapshot.payment_method?.payment_description ?? "", + + recipient: { + name: snapshot.recipient.name, + tin: snapshot.recipient.tin, + format_address: this.formatAddress(snapshot.recipient), + }, + + items: this.itemsBuilder.toOutput(snapshot.items, { locale }), + taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }), + + subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions), + discount_percentage: PercentageDTOHelper.format(snapshot.discount_percentage, locale, { + hideZeros: true, + }), + discount_amount: MoneyDTOHelper.format(snapshot.discount_amount, locale, moneyOptions), + taxable_amount: MoneyDTOHelper.format(snapshot.taxable_amount, locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(snapshot.taxes_amount, locale, moneyOptions), + total_amount: MoneyDTOHelper.format(snapshot.total_amount, locale, moneyOptions), + + verifactu: { + ...snapshot.verifactu, + qr_code: snapshot.verifactu.qr_code.replace("data:image/png;base64,", ""), + }, + }; + } + + private formatAddress(recipient: IssuedInvoiceFullSnapshot["recipient"]): string { + const lines: string[] = []; + + if (recipient.street) lines.push(recipient.street); + if (recipient.street2) lines.push(recipient.street2); + + const cityLine = [recipient.postal_code, recipient.city].filter(Boolean).join(" "); + + if (cityLine) lines.push(cityLine); + if (recipient.province && recipient.province !== recipient.city) { + lines.push(recipient.province); + } + if (recipient.country && recipient.country !== "es") { + lines.push(recipient.country); + } + + return lines.join("\n"); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-tax-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-tax-report-snapshot-builder.ts new file mode 100644 index 00000000..1da56de2 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-tax-report-snapshot-builder.ts @@ -0,0 +1,44 @@ +import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { + IssuedInvoiceFullSnapshot, + IssuedInvoiceReportTaxSnapshot, +} from "../../application-models"; + +export interface IIssuedInvoiceTaxReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceTaxReportSnapshotBuilder + implements IIssuedInvoiceTaxReportSnapshotBuilder +{ + toOutput( + taxes: IssuedInvoiceFullSnapshot["taxes"], + params?: ISnapshotBuilderParams + ): IssuedInvoiceReportTaxSnapshot[] { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return taxes.map((tax) => ({ + taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions), + + iva_code: tax.iva_code, + iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale), + iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions), + + rec_code: tax.rec_code, + rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale), + rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions), + + retention_code: tax.retention_code, + retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale), + retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions), + + taxes_amount: MoneyDTOHelper.format(tax.taxes_amount, locale, moneyOptions), + })); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/get-issued-invoice-by-id.use-case.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/get-issued-invoice-by-id.use-case.ts new file mode 100644 index 00000000..9a9f4e2d --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/get-issued-invoice-by-id.use-case.ts @@ -0,0 +1,50 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IIssuedInvoiceFinder } from "../services/issued-invoice-finder"; +import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders/full"; + +type GetIssuedInvoiceUseCaseInput = { + companyId: UniqueID; + invoice_id: string; +}; + +export class GetIssuedInvoiceByIdUseCase { + constructor( + private readonly finder: IIssuedInvoiceFinder, + private readonly fullSnapshotBuilder: IIssuedInvoiceFullSnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: GetIssuedInvoiceUseCaseInput) { + const { invoice_id, companyId } = params; + + const idOrError = UniqueID.create(invoice_id); + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const invoiceId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const invoiceResult = await this.finder.findIssuedInvoiceById( + companyId, + invoiceId, + transaction + ); + + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoiceResult.data); + + return Result.ok(fullSnapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts similarity index 63% rename from modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts rename to modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts index ea278f6a..e8d3ab9d 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts @@ -1,3 +1,3 @@ -export * from "./get-issued-invoice.use-case"; +export * from "./get-issued-invoice-by-id.use-case"; export * from "./list-issued-invoices.use-case"; export * from "./report-issued-invoices"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts new file mode 100644 index 00000000..da9f1376 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts @@ -0,0 +1,61 @@ +import type { ITransactionManager } from "@erp/core/api"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import type { IIssuedInvoiceFinder } from "../services"; +import type { IIssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders"; + +type ListIssuedInvoicesUseCaseInput = { + companyId: UniqueID; + criteria: Criteria; +}; + +export class ListIssuedInvoicesUseCase { + constructor( + private readonly finder: IIssuedInvoiceFinder, + private readonly itemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: ListIssuedInvoicesUseCaseInput) { + const { criteria, companyId } = params; + + return this.transactionManager.complete(async (transaction: Transaction) => { + try { + const result = await this.finder.findIssuedInvoicesByCriteria( + companyId, + criteria, + transaction + ); + + if (result.isFailure) { + return Result.fail(result.error); + } + + const invoices = result.data; + const totalInvoices = invoices.total(); + + const items = invoices.map((item) => this.itemSnapshotBuilder.toOutput(item)); + + // ????? + const snapshot = { + page: criteria.pageNumber, + per_page: criteria.pageSize, + total_pages: Math.ceil(totalInvoices / criteria.pageSize), + total_items: totalInvoices, + items: items, + metadata: { + entity: "issued-invoices", + criteria: criteria.toJSON(), + }, + }; + + return Result.ok(snapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts similarity index 52% rename from modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts rename to modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts index 9d2fabc2..9d695a7a 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts @@ -1 +1,2 @@ +//export * from "./issued-invoice-document"; export * from "./report-issued-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts new file mode 100644 index 00000000..7a3a7515 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts @@ -0,0 +1,170 @@ +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 { IIssuedInvoiceFullSnapshotBuilder } from "../../snapshot-builders"; +import type { IIssuedInvoiceReportSnapshotBuilder } from "../../snapshot-builders/report"; + +type ReportIssuedInvoiceUseCaseInput = { + companyId: UniqueID; + companySlug: string; + invoice_id: string; + format: RendererFormat; +}; + +export class ReportIssuedInvoiceUseCase { + constructor( + private readonly finder: IIssuedInvoiceFinder, + private readonly fullSnapshotBuilder: IIssuedInvoiceFullSnapshotBuilder, + private readonly reportSnapshotBuilder: IIssuedInvoiceReportSnapshotBuilder, + private readonly documentGenerationService: IssuedInvoiceDocumentGeneratorService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(params: ReportIssuedInvoiceUseCaseInput) { + const { invoice_id, companyId, companySlug, format } = params; + + const idOrError = UniqueID.create(invoice_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const invoiceId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const invoiceResult = await this.finder.findIssuedInvoiceById( + companyId, + invoiceId, + transaction + ); + + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + const invoice = invoiceResult.data; + + // Snapshot completo de la entidad + const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice); + + // Snapshot para informe a partir del anterior + const reportSnapshot = this.reportSnapshotBuilder.toOutput(fullSnapshot, { + languageCode: invoice.languageCode, + }); + + // Llamar al servicio y que se apañe + const documentResult = await this.documentGenerationService.generate({ + companyId, + invoiceId, + snapshot: reportSnapshot, + }); + + if (documentResult.isFailure) { + return Result.fail(documentResult.error); + } + + // 5. Devolver artefacto firmado + return Result.ok({ + payload: documentResult.data.payload, + mimeType: "application/pdf", + filename: documentResult.data.filename, + }); + } catch (error: unknown) { + 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({ + resource: "issued-invoice", + projection: "FULL", + }); + + const reportPresenter = this.presenterRegistry.getPresenter({ + resource: "issued-invoice", + projection: "REPORT", + }); + + const renderer = this.rendererRegistry.getRenderer({ + 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, + filename: `customer-invoice-${invoice.invoiceNumber}.pdf`, + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts deleted file mode 100644 index 2276c1de..00000000 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./issued-invoice.full.presenter"; -export * from "./issued-invoice-items.full.presenter"; -export * from "./issued-invoice-recipient.full.presenter"; -export * from "./issued-invoice-verifactu.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts deleted file mode 100644 index 0ff3db64..00000000 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-recipient.full.presenter.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Presenter } from "@erp/core/api"; -import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; - -import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; -import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; - -type GetIssuedInvoiceRecipientByIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["recipient"]; - -export class IssuedInvoiceRecipientFullPresenter extends Presenter { - toOutput(invoice: CustomerInvoice): GetIssuedInvoiceRecipientByIdResponseDTO { - if (!invoice.recipient) { - throw DomainValidationError.requiredValue("recipient", { - cause: invoice, - }); - } - - return invoice.recipient.match( - (recipient: InvoiceRecipient) => { - return { - id: invoice.customerId.toString(), - name: recipient.name.toString(), - tin: recipient.tin.toString(), - street: toEmptyString(recipient.street, (value) => value.toString()), - street2: toEmptyString(recipient.street2, (value) => value.toString()), - city: toEmptyString(recipient.city, (value) => value.toString()), - province: toEmptyString(recipient.province, (value) => value.toString()), - postal_code: toEmptyString(recipient.postalCode, (value) => value.toString()), - country: toEmptyString(recipient.country, (value) => value.toString()), - }; - }, - () => { - return { - id: "", - name: "", - tin: "", - street: "", - street2: "", - city: "", - province: "", - postal_code: "", - country: "", - }; - } - ); - } -} diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts deleted file mode 100644 index 011078a1..00000000 --- a/modules/customer-invoices/src/api/application/presenters/domain/proformas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./proforma.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/index.ts b/modules/customer-invoices/src/api/application/presenters/index.ts deleted file mode 100644 index 8d7e8eb5..00000000 --- a/modules/customer-invoices/src/api/application/presenters/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./domain"; -export * from "./queries"; -export * from "./reports"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/index.ts b/modules/customer-invoices/src/api/application/presenters/reports/index.ts deleted file mode 100644 index de2b09ca..00000000 --- a/modules/customer-invoices/src/api/application/presenters/reports/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./issued-invoices"; -export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts deleted file mode 100644 index 8433c115..00000000 --- a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./issued-invoice.report.presenter"; -export * from "./issued-invoice-items.report.presenter"; -export * from "./issued-invoice-taxes.report.presenter"; diff --git a/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts b/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts new file mode 100644 index 00000000..0c51785b --- /dev/null +++ b/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts @@ -0,0 +1,24 @@ +import type { UniqueID } from "@repo/rdx-ddd"; + +export interface ISignDocumentCommand { + /** PDF sin firmar */ + readonly file: Buffer; + + /** Identidad estable de la empresa */ + readonly companyId: UniqueID; +} + +export interface IDocumentSigningService { + /** + * Firma un documento PDF. + * + * Invariantes: + * - El input NO se persiste. + * - El output ES el documento legal. + * - No se realizan validaciones de negocio aquí. + * + * Errores: + * - Lanza excepción si la firma falla. + */ + sign(command: ISignDocumentCommand): Promise; +} diff --git a/modules/customer-invoices/src/api/application/services/index.ts b/modules/customer-invoices/src/api/application/services/index.ts index 92829664..2e41928f 100644 --- a/modules/customer-invoices/src/api/application/services/index.ts +++ b/modules/customer-invoices/src/api/application/services/index.ts @@ -1 +1,2 @@ export * from "./customer-invoice-application.service"; +export * from "./document-signing-service.interface"; diff --git a/modules/customer-invoices/src/api/application/services/proforma-factory.ts b/modules/customer-invoices/src/api/application/services/proforma-factory.ts new file mode 100644 index 00000000..8ae192f5 --- /dev/null +++ b/modules/customer-invoices/src/api/application/services/proforma-factory.ts @@ -0,0 +1,27 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import { CustomerInvoice, type CustomerInvoiceProps } from "../../domain/aggregates"; + +export interface IProformaFactory { + /** + * Crea una proforma válida para una empresa a partir de props ya validadas. + * + * No persiste el agregado. + */ + createProforma( + companyId: UniqueID, + props: Omit, + proformaId?: UniqueID + ): Result; +} + +export class CustomerInvoiceFactory implements IProformaFactory { + createProforma( + companyId: UniqueID, + props: Omit, + proformaId?: UniqueID + ): Result { + return CustomerInvoice.create({ ...props, companyId }, proformaId); + } +} diff --git a/modules/customer-invoices/src/api/application/services/proforma-number-service.ts b/modules/customer-invoices/src/api/application/services/proforma-number-service.ts new file mode 100644 index 00000000..5122a125 --- /dev/null +++ b/modules/customer-invoices/src/api/application/services/proforma-number-service.ts @@ -0,0 +1,31 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { + CustomerInvoiceNumber, + CustomerInvoiceSerie, + ICustomerInvoiceNumberGenerator, +} from "../../domain"; + +export interface IProformaNumberService { + /** + * Devuelve el siguiente número disponible para una factura emitida. + */ + nextProformaNumber( + companyId: UniqueID, + series: Maybe, + transaction: unknown + ): Promise>; +} + +export class ProformaNumberService implements IProformaNumberService { + constructor(private readonly numberGenerator: ICustomerInvoiceNumberGenerator) {} + + async nextProformaNumber( + companyId: UniqueID, + series: Maybe, + transaction: unknown + ): Promise> { + return this.numberGenerator.nextForCompany(companyId, series, transaction); + } +} diff --git a/modules/customer-invoices/src/api/application/services/proforma-write-service.ts b/modules/customer-invoices/src/api/application/services/proforma-write-service.ts new file mode 100644 index 00000000..110015f0 --- /dev/null +++ b/modules/customer-invoices/src/api/application/services/proforma-write-service.ts @@ -0,0 +1,135 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import { + CustomerInvoiceIsProformaSpecification, + type CustomerInvoiceStatus, + ProformaCannotBeDeletedError, + StatusInvoiceIsDraftSpecification, +} from "../../domain"; +import type { + CustomerInvoice, + CustomerInvoicePatchProps, + CustomerInvoiceProps, +} from "../../domain/aggregates"; +import type { ICustomerInvoiceRepository } from "../../domain/repositories"; + +import type { IProformaFactory } from "./proforma-factory"; + +export type IIssuedInvoiceWriteService = {}; + +export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService { + constructor( + private readonly repository: ICustomerInvoiceRepository, + private readonly proformaFactory: IProformaFactory + ) {} + + /** + * Crea y persiste una nueva proforma para una empresa. + */ + async createProforma( + companyId: UniqueID, + props: Omit, + transaction: Transaction + ): Promise> { + const invoiceResult = this.proformaFactory.createProforma(companyId, props); + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + return this.repository.create(invoiceResult.data, transaction); + } + + /** + * Actualiza una proforma existente. + */ + async updateProforma( + companyId: UniqueID, + proforma: CustomerInvoice, + transaction: Transaction + ): Promise> { + return this.repository.update(proforma, transaction); + } + + /** + * Aplica cambios parciales a una proforma existente. + * No persiste automáticamente. + */ + async patchProforma( + companyId: UniqueID, + proformaId: UniqueID, + changes: CustomerInvoicePatchProps, + transaction: Transaction + ): Promise> { + const proformaResult = await this.repository.getProformaByIdInCompany( + companyId, + proformaId, + transaction, + {} + ); + + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const updated = proformaResult.data.update(changes); + if (updated.isFailure) { + return Result.fail(updated.error); + } + + return Result.ok(updated.data); + } + + /** + * Elimina (baja lógica) una proforma. + */ + async deleteProforma( + companyId: UniqueID, + proformaId: UniqueID, + transaction: Transaction + ): Promise> { + const proformaResult = await this.repository.getProformaByIdInCompany( + companyId, + proformaId, + transaction, + {} + ); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const proforma = proformaResult.data; + + const isProforma = new CustomerInvoiceIsProformaSpecification(); + if (!(await isProforma.isSatisfiedBy(proforma))) { + return Result.fail(new ProformaCannotBeDeletedError(proformaId.toString(), "not a proforma")); + } + + const isDraft = new StatusInvoiceIsDraftSpecification(); + if (!(await isDraft.isSatisfiedBy(proforma))) { + return Result.fail( + new ProformaCannotBeDeletedError(proformaId.toString(), "status is not 'draft'") + ); + } + + return this.repository.deleteProformaByIdInCompany(companyId, proformaId, transaction); + } + + /** + * Actualiza el estado de una proforma. + */ + async updateProformaStatus( + companyId: UniqueID, + proformaId: UniqueID, + newStatus: CustomerInvoiceStatus, + transaction?: Transaction + ): Promise> { + return this.repository.updateProformaStatusByIdInCompany( + companyId, + proformaId, + newStatus, + transaction + ); + } +} diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/domain/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/index.ts new file mode 100644 index 00000000..ec1298bd --- /dev/null +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/index.ts @@ -0,0 +1 @@ +//export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/index.ts new file mode 100644 index 00000000..8a793490 --- /dev/null +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/index.ts @@ -0,0 +1 @@ +//export * from "./proforma.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts similarity index 96% rename from modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts index 26bfb6a9..9bcc3c88 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts @@ -1,4 +1,4 @@ -import { Presenter } from "@erp/core/api"; +import { SnapshotBuilder } from "@erp/core/api"; import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; import { toEmptyString } from "@repo/rdx-ddd"; import type { ArrayElement } from "@repo/rdx-utils"; @@ -7,7 +7,7 @@ import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../doma type GetProformaItemByIdResponseDTO = ArrayElement; -export class ProformaItemsFullPresenter extends Presenter { +export class ProformaItemsFullPresenter extends SnapshotBuilder { private _mapItem( proformaItem: CustomerInvoiceItem, index: number diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts similarity index 92% rename from modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts index 360bf5de..3a4c0bba 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-recipient.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts @@ -1,4 +1,4 @@ -import { Presenter } from "@erp/core/api"; +import { SnapshotBuilder } from "@erp/core/api"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; import type { GetIssuedInvoiceByIdResponseDTO as GetProformaByIdResponseDTO } from "../../../../../common/dto"; @@ -6,7 +6,7 @@ import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; type GetProformaRecipientByIdResponseDTO = GetProformaByIdResponseDTO["recipient"]; -export class ProformaRecipientFullPresenter extends Presenter { +export class ProformaRecipientFullPresenter extends SnapshotBuilder { toOutput(proforma: CustomerInvoice): GetProformaRecipientByIdResponseDTO { if (!proforma.recipient) { throw DomainValidationError.requiredValue("recipient", { diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts similarity index 100% rename from modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/index.ts new file mode 100644 index 00000000..aaac488b --- /dev/null +++ b/modules/customer-invoices/src/api/application/snapshot-builders/index.ts @@ -0,0 +1,3 @@ +//export * from "./domain"; +//export * from "./queries"; +//export * from "./reports"; diff --git a/modules/customer-invoices/src/api/application/presenters/domain/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/queries/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/presenters/domain/index.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/queries/index.ts diff --git a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/queries/issued-invoices/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/index.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/queries/issued-invoices/index.ts diff --git a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/queries/issued-invoices/issued-invoice.list.presenter.ts similarity index 98% rename from modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/queries/issued-invoices/issued-invoice.list.presenter.ts index c7c5b7e9..730d6339 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/queries/issued-invoices/issued-invoice.list.presenter.ts @@ -1,4 +1,3 @@ -import { Presenter } from "@erp/core/api"; import type { Criteria } from "@repo/rdx-criteria/server"; import { toEmptyString } from "@repo/rdx-ddd"; import type { ArrayElement, Collection } from "@repo/rdx-utils"; diff --git a/modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/queries/proformas/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/presenters/queries/proformas/index.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/queries/proformas/index.ts diff --git a/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/queries/proformas/proforma.list.presenter.ts similarity index 100% rename from modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/queries/proformas/proforma.list.presenter.ts diff --git a/modules/customer-invoices/src/api/application/presenters/queries/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/reports/index.ts similarity index 53% rename from modules/customer-invoices/src/api/application/presenters/queries/index.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/reports/index.ts index de2b09ca..b52bd5d4 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/index.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/reports/index.ts @@ -1,2 +1,2 @@ export * from "./issued-invoices"; -export * from "./proformas"; +//export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/index.ts new file mode 100644 index 00000000..0c5e6734 --- /dev/null +++ b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/index.ts @@ -0,0 +1,3 @@ +//export * from "./issued-invoice.report.presenter"; +//export * from "./issued-invoice-items.report.presenter"; +//export * from "./issued-invoice-taxes.report.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice-items.report.presenter.ts similarity index 92% rename from modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts rename to modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice-items.report.presenter.ts index d447832b..256a2730 100644 --- a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-items.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice-items.report.presenter.ts @@ -1,5 +1,5 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; -import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; +import { type ISnapshotBuilderParams, Presenter } from "@erp/core/api"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { ArrayElement } from "@repo/rdx-utils"; @@ -45,7 +45,7 @@ export class IssuedInvoiceItemsReportPresenter extends Presenter { - try { - const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany( - companyId, - invoiceId, - transaction - ); - - if (invoiceOrError.isFailure) { - return Result.fail(invoiceOrError.error); - } - - const invoice = invoiceOrError.data; - const dto = presenter.toOutput(invoice); - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts deleted file mode 100644 index c92d4482..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/list-issued-invoices.use-case.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import type { Criteria } from "@repo/rdx-criteria/server"; -import type { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; - -import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto"; -import type { IssuedInvoiceListPresenter } from "../../presenters"; -import type { CustomerInvoiceApplicationService } from "../../services"; - -type ListIssuedInvoicesUseCaseInput = { - companyId: UniqueID; - criteria: Criteria; -}; - -export class ListIssuedInvoicesUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) {} - - public execute( - params: ListIssuedInvoicesUseCaseInput - ): Promise> { - const { criteria, companyId } = params; - const presenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "LIST", - }) as IssuedInvoiceListPresenter; - - return this.transactionManager.complete(async (transaction: Transaction) => { - try { - const result = await this.service.findIssuedInvoiceByCriteriaInCompany( - companyId, - criteria, - transaction - ); - - if (result.isFailure) { - return Result.fail(result.error); - } - - const invoices = result.data; - const dto = presenter.toOutput({ - invoices: invoices, - criteria, - }); - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts deleted file mode 100644 index 2214bed3..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/report-issued-invoice.use-case.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { DTO } from "@erp/core"; -import type { - IPresenterRegistry, - IRendererRegistry, - ITransactionManager, - RendererFormat, -} from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { CustomerInvoice } from "../../../../domain"; -import type { CustomerInvoiceApplicationService } from "../../../services"; - -type ReportIssuedInvoiceUseCaseInput = { - companyId: UniqueID; - companySlug: string; - invoice_id: string; - format: RendererFormat; -}; - -export class ReportIssuedInvoiceUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry, - private readonly rendererRegistry: IRendererRegistry - ) {} - - public async execute(params: ReportIssuedInvoiceUseCaseInput) { - const { invoice_id, companyId, companySlug, format } = params; - - const idOrError = UniqueID.create(invoice_id); - - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - const invoiceId = idOrError.data; - - const fullPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "FULL", - }); - - const reportPresenter = this.presenterRegistry.getPresenter({ - resource: "issued-invoice", - projection: "REPORT", - }); - - const renderer = this.rendererRegistry.getRenderer({ - resource: "issued-invoice", - format, - }); - - 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, - filename: `customer-invoice-${invoice.invoiceNumber}.pdf`, - }); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts index 5f597597..e1442635 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/create-proforma.use-case.ts @@ -9,8 +9,8 @@ import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; import type { CreateProformaRequestDTO } from "../../../../../common"; -import type { ProformaFullPresenter } from "../../../presenters"; import type { CustomerInvoiceApplicationService } from "../../../services"; +import type { ProformaFullPresenter } from "../../../snapshot-builders"; import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-proforma-props"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts index 0abe1e72..157f83f1 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts @@ -2,8 +2,8 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { ProformaFullPresenter } from "../../presenters/domain"; import type { CustomerInvoiceApplicationService } from "../../services"; +import type { ProformaFullPresenter } from "../../snapshot-builders/domain"; type GetProformaUseCaseInput = { companyId: UniqueID; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts index 03abe6b6..c3b13d40 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts @@ -5,8 +5,8 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; -import type { ProformaListPresenter } from "../../presenters"; import type { CustomerInvoiceApplicationService } from "../../services"; +import type { ProformaListPresenter } from "../../snapshot-builders"; type ListProformasUseCaseInput = { companyId: UniqueID; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts index 70f092bb..1abcc36b 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts @@ -1,7 +1,7 @@ import { TemplatePresenter } from "@erp/core/api"; import type { CustomerInvoice } from "../../../../../domain"; -import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../presenters"; +import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../snapshot-builders"; export class ProformaReportHTMLPresenter extends TemplatePresenter { toOutput(proforma: CustomerInvoice, params: { companySlug: string }): string { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts index bba5c439..17c40c7a 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts @@ -5,8 +5,8 @@ import type { Transaction } from "sequelize"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; import type { CustomerInvoicePatchProps } from "../../../../domain"; -import type { ProformaFullPresenter } from "../../../presenters"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; +import type { ProformaFullPresenter } from "../../../snapshot-builders"; import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; diff --git a/modules/customer-invoices/src/api/di.ts b/modules/customer-invoices/src/api/di.ts new file mode 100644 index 00000000..c126b968 --- /dev/null +++ b/modules/customer-invoices/src/api/di.ts @@ -0,0 +1,29 @@ +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; diff --git a/modules/customer-invoices/src/api/index.ts b/modules/customer-invoices/src/api/index.ts index 2a84dffb..63e7ee29 100644 --- a/modules/customer-invoices/src/api/index.ts +++ b/modules/customer-invoices/src/api/index.ts @@ -1,9 +1,7 @@ import type { IModuleServer, ModuleParams } from "@erp/core/api"; -import type { UniqueID } from "@repo/rdx-ddd"; -import type { Transaction } from "sequelize"; -import { models, proformasRouter } from "./infrastructure"; -import { issuedInvoicesRouter } from "./infrastructure/express/issued-invoices.routes"; +import { models } from "./infrastructure"; +import { issuedInvoicesRouter } from "./infrastructure/express/issued-invoices/issued-invoices.routes"; export const customerInvoicesAPIModule: IModuleServer = { name: "customer-invoices", @@ -11,17 +9,48 @@ export const customerInvoicesAPIModule: IModuleServer = { dependencies: ["customers"], async init(params: ModuleParams) { - // const contacts = getService("contacts"); - const { logger } = params; - proformasRouter(params); - issuedInvoicesRouter(params); + const { logger, env } = params; + + //proformasRouter(params); + + issuedInvoicesRouter({ + ...params, + services: issuedInvoicesServices, + }); + logger.info("🚀 CustomerInvoices module initialized", { label: this.name }); }, - async registerDependencies(params) { - const { logger } = params; /* = ModuleParams & { + 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, @@ -30,15 +59,7 @@ export const customerInvoicesAPIModule: IModuleServer = { return { models, services: { - getInvoiceByIdInCompany: ( - companyId: UniqueID, - invoiceId: UniqueID, - transaction?: Transaction - ) => { - /*const { service } = deps; - - return service.getInvoiceByIdInCompany(companyId, invoiceId, transaction);*/ - }, + issuedInvoices: issuedInvoicesServices, }, }; }, diff --git a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts new file mode 100644 index 00000000..3beb4300 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts @@ -0,0 +1,44 @@ +import type { + FastReportRenderer, + IDocumentCacheStore, + IDocumentSigningService, + IDocumentStorage, + ISigningContextResolver, +} 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; + + const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = { + fastReportRenderer: documentRenderers.fastReportRenderer, + + // + signingContextResolver: documentSigning.signingContextResolver, + documentSigningService: documentSigning.signingService, + + // + documentCacheStore: documentStorage.cacheStore, + documentStorage: documentStorage.storage, + }; + + const documentGeneratorPipeline = IssuedInvoiceDocumentPipelineFactory.create(pipelineDeps); + + return documentGeneratorPipeline; +}; diff --git a/modules/customer-invoices/src/api/infrastructure/di/index.ts b/modules/customer-invoices/src/api/infrastructure/di/index.ts new file mode 100644 index 00000000..5d10b1e3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/di/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoices.di"; +//export * from "./proformas.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts b/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts new file mode 100644 index 00000000..5ae8f275 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts @@ -0,0 +1,133 @@ +// modules/invoice/infrastructure/invoice-dependencies.factory.ts + +import { + type IDocumentSigningService, + type ITransactionManager, + type ModuleParams, + buildCoreDocumentsDI, + buildTransactionManager, +} from "@erp/core/api"; + +import { + type GetIssuedInvoiceByIdUseCase, + type ListIssuedInvoicesUseCase, + type ReportIssuedInvoiceUseCase, + buildGetIssuedInvoiceByIdUseCase, + buildIssuedInvoiceFinder, + buildIssuedInvoiceSnapshotBuilders, + buildListIssuedInvoicesUseCase, + buildReportIssuedInvoiceUseCase, +} from "../../application/issued-invoices"; +import { IssuedInvoiceDocumentPipelineFactory } from "../documents"; + +import { buildRepository } from "./repositories.di"; + +export type IssuedInvoicesDeps = { + useCases: { + list_issued_invoices: () => ListIssuedInvoicesUseCase; + get_issued_invoice_by_id: () => GetIssuedInvoiceByIdUseCase; + report_issued_invoice: () => ReportIssuedInvoiceUseCase; + }; +}; + +export function buildIssuedInvoicesDI(params: ModuleParams) { + const { database, env } = params; + const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env); + + const transactionManager = buildTransactionManager(database); + const repository = buildRepository(database); + + // -------------------------------------------------------------------------- + // 4️⃣ Finder + snapshot builders (APPLICATION) + // -------------------------------------------------------------------------- + + const finder = buildIssuedInvoiceFinder(repository); + const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders(); + + const issuedInvoiceDocumentPipeline = new IssuedInvoiceDocumentPipelineFactory({ + renderer: documentRenderers.fastReportRenderer, + }).create; + + // -------------------------------------------------------------------------- + // 5️⃣ Use Case + // -------------------------------------------------------------------------- + + 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: 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, + transactionManager, + }), + }, + }; +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts b/modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts similarity index 90% rename from modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts rename to modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts index cffd9e73..bdd56c93 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas-dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts @@ -24,17 +24,20 @@ import { ReportProformaUseCase, UpdateProformaUseCase, } from "../application"; -import { ProformaItemsFullPresenter } from "../application/presenters/domain/proformas/proforma-items.full.presenter"; -import { ProformaRecipientFullPresenter } from "../application/presenters/domain/proformas/proforma-recipient.full.presenter"; +import { ProformaItemsFullPresenter } from "../application/snapshot-builders/domain/proformas/proforma-items.full.presenter"; +import { ProformaRecipientFullPresenter } from "../application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter"; import { IssuedInvoiceTaxesReportPresenter, ProformaItemsReportPresenter, ProformaReportPresenter, -} from "../application/presenters/reports"; +} from "../application/snapshot-builders/reports"; -import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; +import { SequelizeInvoiceNumberGenerator } from "./persistence/sequelize"; +import { + CustomerInvoiceDomainMapper, + CustomerInvoiceListMapper, +} from "./persistence/sequelize/mappers"; import { CustomerInvoiceRepository } from "./sequelize"; -import { SequelizeInvoiceNumberGenerator } from "./services"; export type ProformasDeps = { transactionManager: SequelizeTransactionManager; diff --git a/modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts new file mode 100644 index 00000000..51ecfa47 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts @@ -0,0 +1,29 @@ +import { SpainTaxCatalogProvider } from "@erp/core"; +import { InMemoryMapperRegistry } from "@erp/core/api"; +import type { Sequelize } from "sequelize"; + +import { + CustomerInvoiceDomainMapper, + CustomerInvoiceListMapper, + CustomerInvoiceRepository, +} from "../persistence"; + +export const buildRepository = (database: Sequelize) => { + const mapperRegistry = new InMemoryMapperRegistry(); + + const taxCatalog = SpainTaxCatalogProvider(); + + mapperRegistry + .registerDomainMapper( + { resource: "customer-invoice" }, + new CustomerInvoiceDomainMapper({ taxCatalog }) + ) + .registerQueryMappers([ + { + key: { resource: "customer-invoice", query: "LIST" }, + mapper: new CustomerInvoiceListMapper(), + }, + ]); + + return new CustomerInvoiceRepository({ mapperRegistry, database }); +}; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/index.ts new file mode 100644 index 00000000..5f7e3a8d --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/index.ts @@ -0,0 +1,2 @@ +export * from "./pipelines"; +export * from "./renderers"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/index.ts new file mode 100644 index 00000000..838dbd6f --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-document-pipeline-factory.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts new file mode 100644 index 00000000..d3355975 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts @@ -0,0 +1,77 @@ +import { + DocumentGenerationService, + DocumentPostProcessorChain, + type FastReportRenderer, + type IDocumentCacheStore, + type IDocumentPostProcessor, + type IDocumentSideEffect, + type IDocumentSigningService, + type IDocumentStorage, + type ISigningContextResolver, +} from "@erp/core/api"; + +import { + type IssuedInvoiceDocumentGeneratorService, + IssuedInvoiceDocumentMetadataFactory, + type IssuedInvoiceReportSnapshot, +} from "../../../application"; +import { DigitalSignaturePostProcessor } from "../post-processors"; +import { IssuedInvoiceSignedDocumentCachePreProcessor } from "../pre-processors"; +import { IssuedInvoiceDocumentRenderer } from "../renderers"; +import { PersistIssuedInvoiceDocumentSideEffect } from "../side-effects"; + +/** + * Factory de pipeline de generación de documentos + * para facturas emitidas (Issued Invoice). + */ + +export interface IssuedInvoiceDocumentPipelineFactoryDeps { + // Core / Infra + fastReportRenderer: FastReportRenderer; + //templateResolver: IDocumentTemplateResolver; + + signingContextResolver: ISigningContextResolver; + documentSigningService: IDocumentSigningService; + + documentCacheStore: IDocumentCacheStore; + documentStorage: IDocumentStorage; +} + +export class IssuedInvoiceDocumentPipelineFactory { + static create( + deps: IssuedInvoiceDocumentPipelineFactoryDeps + ): IssuedInvoiceDocumentGeneratorService { + // 1. Pre-processors (cache firmado) + const preProcessors = [ + new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentCacheStore), + ]; + + // 2. Renderer (FastReport) + const documentRenderer = new IssuedInvoiceDocumentRenderer( + deps.fastReportRenderer, + "/templates/issued-invoice.frx" + ); + + // 3) Metadata factory (Application) + const metadataFactory = new IssuedInvoiceDocumentMetadataFactory(); + + // 3) Firma real (Core / Infra) + const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([ + new DigitalSignaturePostProcessor(deps.signingContextResolver, deps.documentSigningService), + ]); + + // 4. Side-effects (persistencia best-effort) + const sideEffects: IDocumentSideEffect[] = [ + new PersistIssuedInvoiceDocumentSideEffect(deps.documentStorage), + ]; + + // 5. Pipeline final + return new DocumentGenerationService( + metadataFactory, + preProcessors, + documentRenderer, + postProcessor, + sideEffects + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts new file mode 100644 index 00000000..3fb2e0fa --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts @@ -0,0 +1,41 @@ +import type { + ICertificateResolver, + IDocument, + IDocumentMetadata, + IDocumentPostProcessor, + IDocumentSigningService, +} from "@erp/core/api"; + +/** + * Post-processor que firma digitalmente un documento PDF. + * + * - Infra-level + * - Falla si no puede firmar + * - No persiste ni cachea + */ +export class DigitalSignaturePostProcessor implements IDocumentPostProcessor { + constructor( + private readonly certificateResolver: ICertificateResolver, + private readonly signingService: IDocumentSigningService + ) {} + + async process(document: IDocument, metadata: IDocumentMetadata): Promise { + // Validación defensiva mínima + if (document.mimeType !== "application/pdf") { + throw new Error("DigitalSignaturePostProcessor can only sign PDF documents"); + } + + // 1. Resolver certificado de la empresa + const certificate = await this.certificateResolver.resolveForCompany(metadata.companyId); + + // 2. Firmar payload + const signedPayload = await this.signingService.sign(document.payload, certificate); + + // 3. Devolver nuevo documento (inmutable) + return { + payload: signedPayload, + mimeType: document.mimeType, + filename: document.filename, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/index.ts new file mode 100644 index 00000000..82543f53 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/post-processors/index.ts @@ -0,0 +1 @@ +export * from "./digital-signature-post-processor"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/index.ts new file mode 100644 index 00000000..16bb0722 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-signed-document-cache-pre-processor"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts new file mode 100644 index 00000000..9f16434e --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts @@ -0,0 +1,73 @@ +import { + DocumentCacheKeyFactory, + type IDocument, + type IDocumentCacheStore, + type IDocumentMetadata, + type IDocumentPreProcessor, +} from "@erp/core/api"; + +/** + * Pre-processor de cache técnico para documentos firmados + * de facturas emitidas. + * + * - Best-effort + * - Nunca rompe el flujo + * - Invalida cache corrupto + */ +export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPreProcessor { + constructor(private readonly cache: IDocumentCacheStore) {} + + async tryResolve(metadata: IDocumentMetadata): Promise { + if (!metadata.cacheKey) { + return null; + } + + try { + const cacheKey = DocumentCacheKeyFactory.fromMetadata(metadata); + + return await this.cache.get(cacheKey); + } catch { + // best-effort: cualquier fallo se trata como cache miss + return null; + } + + if (!metadata.cacheKey) { + return null; + } + + try { + const exists = await this.cache.exists(metadata.cacheKey); + if (!exists) { + return null; + } + + const document = await this.cache.read(metadata.cacheKey); + + if (!this.isValid(document)) { + await this.cache.invalidate(metadata.cacheKey); + return null; + } + + return document; + } catch { + // Cache failure → ignore and continue pipeline + return null; + } + } + + /** + * Validación mínima de integridad. + * No valida firma criptográfica. + */ + private isValid(document: IDocument): boolean { + if (!document.payload || document.payload.length === 0) { + return false; + } + + if (document.mimeType !== "application/pdf") { + return false; + } + + return true; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/index.ts new file mode 100644 index 00000000..827479a3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-document-renderer.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts new file mode 100644 index 00000000..22ef472f --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts @@ -0,0 +1,44 @@ +import type { FastReportRenderer, IDocument, IDocumentRenderer } from "@erp/core/api"; + +import type { IssuedInvoiceReportSnapshot } from "../../../../application"; + +/** + * Adaptador Application → Infra para la generación del documento + * PDF de una factura emitida (Issued Invoice). + * + * - Recibe snapshot de report + * - Invoca FastReportRenderer + * - Devuelve IDocument + * + * NO captura errores: FastReportError se propaga. + */ +export class IssuedInvoiceDocumentRenderer + implements IDocumentRenderer +{ + constructor( + private readonly fastReportRenderer: FastReportRenderer, + private readonly templatePath: string + ) {} + + async render(snapshot: IssuedInvoiceReportSnapshot): Promise { + const output = await this.fastReportRenderer.render({ + templatePath: this.templatePath, + inputData: snapshot, + format: "PDF", + }); + + return { + payload: this.normalizePayload(output.payload), + mimeType: "application/pdf", + filename: "issued-invoice.pdf", + }; + } + + /** + * Normaliza la salida de FastReport a Buffer. + * FastReport puede devolver string o Buffer. + */ + private normalizePayload(payload: Buffer | string): Buffer { + return Buffer.isBuffer(payload) ? payload : Buffer.from(payload); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/proforma-fastreport.renderer.ts b/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/proforma-fastreport.renderer.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/renderers/index.ts new file mode 100644 index 00000000..3d635d3a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/renderers/index.ts @@ -0,0 +1 @@ +export * from "./fastreport"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/index.ts b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/index.ts new file mode 100644 index 00000000..65d94d9c --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/index.ts @@ -0,0 +1 @@ +export * from "./persist-issued-invoice-document-side-effect"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts new file mode 100644 index 00000000..d9a3fd53 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts @@ -0,0 +1,27 @@ +import type { + IDocument, + IDocumentMetadata, + IDocumentSideEffect, + IDocumentStorage, +} from "@erp/core/api"; + +/** + * Side-effect de persistencia best-effort del documento final + * de una factura emitida. + * + * - Nunca rompe el flujo + * - Usa cacheKey/metadata para decidir la clave + */ +export class PersistIssuedInvoiceDocumentSideEffect implements IDocumentSideEffect { + constructor(private readonly storage: IDocumentStorage) {} + + async execute(document: IDocument, metadata: IDocumentMetadata): Promise { + // Si no hay cacheKey, no se persiste + if (!metadata.cacheKey) { + return; + } + + // Persistencia best-effort + await this.storage.save(document, metadata); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts deleted file mode 100644 index de2b09ca..00000000 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./issued-invoices"; -export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/index.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/index.ts deleted file mode 100644 index 9a41c812..00000000 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./get-issued-invoice.controller"; -export * from "./list-issued-invoices.controller"; -export * from "./report-issued-invoice.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/index.ts b/modules/customer-invoices/src/api/infrastructure/express/index.ts index f8fd815e..b52bd5d4 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/index.ts @@ -1,2 +1,2 @@ -export * from "./issued-invoices.routes"; -export * from "./proformas.routes"; +export * from "./issued-invoices"; +//export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/get-issued-invoice-by-id.controller.ts similarity index 59% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/get-issued-invoice-by-id.controller.ts index 6b2aeaf0..7ca1c0bc 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/get-issued-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/get-issued-invoice-by-id.controller.ts @@ -5,11 +5,12 @@ import { requireCompanyContextGuard, } from "@erp/core/api"; -import type { GetIssuedInvoiceUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common"; +import type { GetIssuedInvoiceByIdUseCase } from "../../../../application/issued-invoices"; +import { customerInvoicesApiErrorMapper } from "../../proformas/proformas-api-error-mapper.ts"; -export class GetIssueInvoiceController extends ExpressController { - public constructor(private readonly useCase: GetIssuedInvoiceUseCase) { +export class GetIssuedInvoiceByIdController extends ExpressController { + public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) { super(); this.errorMapper = customerInvoicesApiErrorMapper; @@ -31,7 +32,10 @@ export class GetIssueInvoiceController extends ExpressController { const result = await this.useCase.execute({ invoice_id, companyId }); return result.match( - (data) => this.ok(data), + (data) => { + const dto = GetIssuedInvoiceByIdResponseSchema.parse(data); + return this.ok(dto); + }, (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/index.ts new file mode 100644 index 00000000..3c0009de --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/index.ts @@ -0,0 +1,3 @@ +export * from "./get-issued-invoice-by-id.controller"; +//export * from "./list-issued-invoices.controller"; +//export * from "./report-issued-invoice.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/list-issued-invoices.controller.ts similarity index 72% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/list-issued-invoices.controller.ts index b1faa7f8..e695fa93 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/list-issued-invoices.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/list-issued-invoices.controller.ts @@ -6,8 +6,9 @@ import { } from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; -import type { ListIssuedInvoicesUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { ListIssuedInvoicesResponseSchema } from "../../../../../common"; +import type { ListIssuedInvoicesUseCase } from "../../../../application/issued-invoices"; +import { customerInvoicesApiErrorMapper } from "../../proformas/proformas-api-error-mapper.ts"; export class ListIssuedInvoicesController extends ExpressController { public constructor(private readonly useCase: ListIssuedInvoicesUseCase) { @@ -48,13 +49,15 @@ export class ListIssuedInvoicesController extends ExpressController { const result = await this.useCase.execute({ criteria, companyId }); return result.match( - (data) => - this.ok(data, { - "X-Total-Count": String(data.total_items), - "Pagination-Count": String(data.total_pages), - "Pagination-Page": String(data.page), - "Pagination-Limit": String(data.per_page), - }), + (data) => { + const dto = ListIssuedInvoicesResponseSchema.parse(data); + this.ok(dto, { + "X-Total-Count": String(dto.total_items), + "Pagination-Count": String(dto.total_pages), + "Pagination-Page": String(dto.page), + "Pagination-Limit": String(dto.per_page), + }); + }, (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/report-issued-invoice.controller.ts similarity index 94% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/report-issued-invoice.controller.ts index 8a16da80..d6b008ca 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issued-invoices/report-issued-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/controllers/report-issued-invoice.controller.ts @@ -8,7 +8,7 @@ import { import type { ReportIssueInvoiceByIdQueryRequestDTO } from "@erp/customer-invoices/common"; import type { ReportIssuedInvoiceUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../../proformas/proformas-api-error-mapper.ts"; export class ReportIssuedInvoiceController extends ExpressController { public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/index.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/index.ts new file mode 100644 index 00000000..e6434f14 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./issued-invoices.routes"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices-api-error-mapper.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts rename to modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices-api-error-mapper.ts diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts similarity index 84% rename from modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts rename to modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts index d6a37f3f..1edc608d 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts @@ -9,14 +9,12 @@ import { ListIssuedInvoicesRequestSchema, ReportIssueInvoiceByIdParamsRequestSchema, ReportIssueInvoiceByIdQueryRequestSchema, -} from "../../../common/dto"; -import { buildIssuedInvoicesDependencies } from "../issued-invoices-dependencies"; +} from "../../../../common/dto"; +import { buildIssuedInvoicesDependencies } from "../../di"; -import { - GetIssueInvoiceController, - ListIssuedInvoicesController, - ReportIssuedInvoiceController, -} from "./controllers"; +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) => { const { app, baseRoutePath, logger } = params as { @@ -53,7 +51,7 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { validateRequest(ListIssuedInvoicesRequestSchema, "params"), async (req: Request, res: Response, next: NextFunction) => { const useCase = deps.useCases.list_issued_invoices(); - const controller = new ListIssuedInvoicesController(useCase /*, deps.presenters.list */); + const controller = new ListIssuedInvoicesController(useCase); return controller.execute(req, res, next); } ); @@ -63,8 +61,8 @@ export const issuedInvoicesRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(GetIssueInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.get_issued_invoice(); - const controller = new GetIssueInvoiceController(useCase); + const useCase = deps.useCases.get_issued_invoice_by_id(); + const controller = new GetIssuedInvoiceByIdController(useCase); return controller.execute(req, res, next); } ); diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/change-status-proforma.controller.ts similarity index 93% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/change-status-proforma.controller.ts index b0ed8ded..a768fda4 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/change-status-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/change-status-proforma.controller.ts @@ -7,7 +7,7 @@ import { import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto"; import type { ChangeStatusProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class ChangeStatusProformaController extends ExpressController { public constructor(private readonly useCase: ChangeStatusProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/create-proforma.controller.ts similarity index 92% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/create-proforma.controller.ts index 8566b702..ff364fd6 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/create-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/create-proforma.controller.ts @@ -7,7 +7,7 @@ import { import type { CreateProformaRequestDTO } from "../../../../../common/dto"; import type { CreateProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class CreateProformaController extends ExpressController { public constructor(private readonly useCase: CreateProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/delete-proforma.controller.ts similarity index 92% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/delete-proforma.controller.ts index 49d9bfd0..ccf5d17e 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/delete-proforma.controller.ts @@ -6,7 +6,7 @@ import { } from "@erp/core/api"; import type { DeleteProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class DeleteProformaController extends ExpressController { public constructor(private readonly useCase: DeleteProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/get-proforma.controller.ts similarity index 91% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/get-proforma.controller.ts index 75d49ff3..e45e3695 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/get-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/get-proforma.controller.ts @@ -6,7 +6,7 @@ import { } from "@erp/core/api"; import type { GetProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class GetProformaController extends ExpressController { public constructor(private readonly useCase: GetProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/index.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/index.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/issue-proforma.controller.ts similarity index 92% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/issue-proforma.controller.ts index 1c219955..96c7882a 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/issue-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/issue-proforma.controller.ts @@ -6,7 +6,7 @@ import { } from "@erp/core/api"; import type { IssueProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class IssueProformaController extends ExpressController { public constructor(private readonly useCase: IssueProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/list-proformas.controller.ts similarity index 94% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/list-proformas.controller.ts index c0925f08..907e52f1 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/list-proformas.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/list-proformas.controller.ts @@ -7,7 +7,7 @@ import { import { Criteria } from "@repo/rdx-criteria/server"; import type { ListProformasUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class ListProformasController extends ExpressController { public constructor(private readonly useCase: ListProformasUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/report-proforma.controller.ts similarity index 93% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/report-proforma.controller.ts index 0f07181b..7841a733 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/report-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/report-proforma.controller.ts @@ -6,7 +6,7 @@ import { } from "@erp/core/api"; import type { ReportProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class ReportProformaController extends ExpressController { public constructor(private readonly useCase: ReportProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/update-proforma.controller.ts similarity index 93% rename from modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/update-proforma.controller.ts index c939f506..e4d2e4da 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/controllers/update-proforma.controller.ts @@ -7,7 +7,7 @@ import { import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; import type { UpdateProformaUseCase } from "../../../../application"; -import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; +import { customerInvoicesApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class UpdateProformaController extends ExpressController { public constructor(private readonly useCase: UpdateProformaUseCase) { diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas/index.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/index.ts new file mode 100644 index 00000000..7e5d4498 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/index.ts @@ -0,0 +1,3 @@ +export * from "./controllers"; +export * from "./proformas.routes"; +export * from "./proformas-api-error-mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas-api-error-mapper.ts new file mode 100644 index 00000000..243ac245 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas-api-error-mapper.ts @@ -0,0 +1,77 @@ +// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError +// (si defines un error más ubicuo dentro del BC con su propia clase) + +import { + ApiErrorMapper, + ConflictApiError, + type ErrorToApiRule, + ValidationApiError, +} from "@erp/core/api"; + +import { + type CustomerInvoiceIdAlreadyExistsError, + type EntityIsNotProformaError, + type InvalidProformaTransitionError, + type ProformaCannotBeConvertedToInvoiceError, + isCustomerInvoiceIdAlreadyExistsError, + isEntityIsNotProformaError, + isInvalidProformaTransitionError, + isProformaCannotBeConvertedToInvoiceError, + isProformaCannotBeDeletedError, +} from "../../../domain"; + +// Crea una regla específica (prioridad alta para sobreescribir mensajes) +const invoiceDuplicateRule: ErrorToApiRule = { + priority: 120, + matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e), + build: (e) => + new ConflictApiError( + (e as CustomerInvoiceIdAlreadyExistsError).message || + "Invoice with the provided id already exists." + ), +}; + +const entityIsNotProformaError: ErrorToApiRule = { + priority: 120, + matches: (e) => isEntityIsNotProformaError(e), + build: (e) => + new ValidationApiError( + (e as EntityIsNotProformaError).message || "Entity with the provided id is not proforma" + ), +}; + +const proformaTransitionRule: ErrorToApiRule = { + priority: 120, + matches: (e) => isInvalidProformaTransitionError(e), + build: (e) => + new ValidationApiError( + (e as InvalidProformaTransitionError).message || "Invalid transition for proforma." + ), +}; + +const proformaConversionRule: ErrorToApiRule = { + priority: 120, + matches: (e) => isProformaCannotBeConvertedToInvoiceError(e), + build: (e) => + new ValidationApiError( + (e as ProformaCannotBeConvertedToInvoiceError).message || + "Proforma cannot be converted to an Invoice." + ), +}; + +const proformaCannotBeDeletedRule: ErrorToApiRule = { + priority: 120, + matches: (e) => isProformaCannotBeDeletedError(e), + build: (e) => + new ValidationApiError( + (e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted." + ), +}; + +// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra +export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() + .register(invoiceDuplicateRule) + .register(entityIsNotProformaError) + .register(proformaConversionRule) + .register(proformaCannotBeDeletedRule) + .register(proformaTransitionRule); diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts similarity index 97% rename from modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts rename to modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts index 3cca5b32..095f422f 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts @@ -16,8 +16,8 @@ import { ReportProformaByIdQueryRequestSchema, UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, -} from "../../../common"; -import { buildProformasDependencies } from "../proformas-dependencies"; +} from "../../../../common"; +import { buildProformasDependencies } from "../../proformas-dependencies"; import { ChangeStatusProformaController, @@ -28,7 +28,7 @@ import { ListProformasController, ReportProformaController, UpdateProformaController, -} from "./controllers/proformas"; +} from "./controllers"; export const proformasRouter = (params: ModuleParams) => { const { app, baseRoutePath, logger } = params as { diff --git a/modules/customer-invoices/src/api/infrastructure/index.ts b/modules/customer-invoices/src/api/infrastructure/index.ts index 1b90dabf..d5b95078 100644 --- a/modules/customer-invoices/src/api/infrastructure/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/index.ts @@ -1,4 +1,5 @@ +export * from "./di"; +export * from "./documents"; export * from "./express"; -export * from "./mappers"; -export * from "./proformas-dependencies"; -export * from "./sequelize"; +export * from "./persistence"; +export * from "./renderers"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts deleted file mode 100644 index f54fe1dd..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices-dependencies.ts +++ /dev/null @@ -1,180 +0,0 @@ -// modules/invoice/infrastructure/invoice-dependencies.factory.ts - -import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core"; -import type { - IMapperRegistry, - IPresenterRegistry, - IRendererRegistry, - ModuleParams, -} from "@erp/core/api"; -import { - FastReportExecutableResolver, - FastReportProcessRunner, - FastReportTemplateResolver, - FileSystemReportStorage, - InMemoryMapperRegistry, - InMemoryPresenterRegistry, - InMemoryRendererRegistry, - SequelizeTransactionManager, -} from "@erp/core/api"; - -import { - CustomerInvoiceApplicationService, - GetIssuedInvoiceUseCase, - IssuedInvoiceFullPresenter, - IssuedInvoiceItemsFullPresenter, - IssuedInvoiceItemsReportPresenter, - IssuedInvoiceListPresenter, - IssuedInvoiceRecipientFullPresenter, - IssuedInvoiceReportPresenter, - IssuedInvoiceTaxesReportPresenter, - IssuedInvoiceVerifactuFullPresenter, - ListIssuedInvoicesUseCase, - ReportIssuedInvoiceUseCase, -} from "../application"; - -import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; -import { IssuedInvoiceReportJSONRenderer } from "./renderers"; -import { IssuedInvoiceReportPDFRenderer } from "./renderers/issued-invoice-report-pdf.renderer"; -import { CustomerInvoiceRepository } from "./sequelize"; -import { SequelizeInvoiceNumberGenerator } from "./services"; - -export type IssuedInvoicesDeps = { - transactionManager: SequelizeTransactionManager; - mapperRegistry: IMapperRegistry; - presenterRegistry: IPresenterRegistry; - rendererRegistry: IRendererRegistry; - repo: CustomerInvoiceRepository; - appService: CustomerInvoiceApplicationService; - catalogs: { - taxes: JsonTaxCatalogProvider; - }; - useCases: { - list_issued_invoices: () => ListIssuedInvoicesUseCase; - get_issued_invoice: () => GetIssuedInvoiceUseCase; - report_issued_invoice: () => ReportIssuedInvoiceUseCase; - }; -}; - -export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesDeps { - const { database, env } = params; - const templateRootPath = env.TEMPLATES_PATH; - const documentRootPath = env.DOCUMENTS_PATH; - - /** Dominio */ - const catalogs = { taxes: SpainTaxCatalogProvider() }; - - /** Infraestructura */ - const transactionManager = new SequelizeTransactionManager(database); - - const mapperRegistry = new InMemoryMapperRegistry(); - mapperRegistry - .registerDomainMapper( - { resource: "customer-invoice" }, - new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes }) - ) - .registerQueryMappers([ - { - key: { resource: "customer-invoice", query: "LIST" }, - mapper: new CustomerInvoiceListMapper(), - }, - ]); - - // Repository & Services - const repository = new CustomerInvoiceRepository({ mapperRegistry, database }); - const numberGenerator = new SequelizeInvoiceNumberGenerator(); - - // Renderers Registry - const frExecResolver = new FastReportExecutableResolver(); - const frProcessRunner = new FastReportProcessRunner(); - const frTemplateResolver = new FastReportTemplateResolver(templateRootPath); - - const rendererRegistry = new InMemoryRendererRegistry(); - const reportStorage = new FileSystemReportStorage(documentRootPath); - rendererRegistry.registerRenderers([ - { - key: { resource: "issued-invoice", format: "PDF" }, - renderer: new IssuedInvoiceReportPDFRenderer( - frExecResolver, - frProcessRunner, - frTemplateResolver, - reportStorage - ), - }, - { - key: { resource: "issued-invoice", format: "JSON" }, - renderer: new IssuedInvoiceReportJSONRenderer(), - }, - ]); - - /** Aplicación */ - const appService = new CustomerInvoiceApplicationService(repository, numberGenerator); - - // Presenter Registry - const presenterRegistry = new InMemoryPresenterRegistry(); - presenterRegistry.registerPresenters([ - // FULL - { - key: { resource: "issued-invoice-items", projection: "FULL" }, - presenter: new IssuedInvoiceItemsFullPresenter(presenterRegistry), - }, - { - key: { resource: "issued-invoice-recipient", projection: "FULL" }, - presenter: new IssuedInvoiceRecipientFullPresenter(presenterRegistry), - }, - { - key: { resource: "issued-invoice-verifactu", projection: "FULL" }, - presenter: new IssuedInvoiceVerifactuFullPresenter(presenterRegistry), - }, - { - key: { resource: "issued-invoice", projection: "FULL" }, - presenter: new IssuedInvoiceFullPresenter(presenterRegistry), - }, - - // LIST - { - key: { resource: "issued-invoice", projection: "LIST" }, - presenter: new IssuedInvoiceListPresenter(presenterRegistry), - }, - - // REPORT - { - key: { resource: "issued-invoice", projection: "REPORT", format: "DTO" }, - presenter: new IssuedInvoiceReportPresenter(presenterRegistry), - }, - { - key: { resource: "issued-invoice-taxes", projection: "REPORT", format: "DTO" }, - presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), - }, - { - key: { resource: "issued-invoice-items", projection: "REPORT", format: "DTO" }, - presenter: new IssuedInvoiceItemsReportPresenter(presenterRegistry), - }, - ]); - - const useCases: IssuedInvoicesDeps["useCases"] = { - // Issue Invoices - list_issued_invoices: () => - new ListIssuedInvoicesUseCase(appService, transactionManager, presenterRegistry), - get_issued_invoice: () => - new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry), - report_issued_invoice: () => - new ReportIssuedInvoiceUseCase( - appService, - transactionManager, - presenterRegistry, - rendererRegistry - ), - }; - - return { - transactionManager, - repo: repository, - mapperRegistry, - presenterRegistry, - rendererRegistry, - appService, - catalogs, - useCases, - }; -} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/mappers/index.ts deleted file mode 100644 index 9e03d7a9..00000000 --- a/modules/customer-invoices/src/api/infrastructure/mappers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./domain"; -export * from "./queries"; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts similarity index 79% rename from modules/customer-invoices/src/api/infrastructure/sequelize/index.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts index 40be26b4..dbc54f46 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts @@ -3,8 +3,10 @@ import customerInvoiceItemModelInit from "./models/customer-invoice-item.model"; import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model"; import verifactuRecordModelInit from "./models/verifactu-record.model"; -export * from "./customer-invoice.repository"; +export * from "./mappers"; export * from "./models"; +export * from "./repositories/customer-invoice.repository"; +export * from "./sequelize-invoice-number-generator"; // Array de inicializadores para que registerModels() lo use export const models = [ diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts similarity index 99% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts index a0ab05ed..4ce85708 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts @@ -25,11 +25,11 @@ import { ItemDiscount, ItemQuantity, ItemTaxGroup, -} from "../../../domain"; +} from "../../../../../domain"; import type { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel, -} from "../../sequelize"; +} from "../../../../sequelize"; export interface ICustomerInvoiceItemDomainMapper extends ISequelizeDomainMapper< diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts index f83707b6..6b918bee 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts @@ -3,11 +3,11 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { UniqueID, type ValidationErrorDetail, toNullable } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { CustomerInvoice, InvoiceTaxGroup } from "../../../domain"; +import type { CustomerInvoice, InvoiceTaxGroup } from "../../../../../domain"; import type { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel, -} from "../../sequelize"; +} from "../../../../sequelize"; /** * Mapper para customer_invoice_taxes diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts index 3747705a..5fe8fa49 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts @@ -26,8 +26,11 @@ import { CustomerInvoiceSerie, CustomerInvoiceStatus, InvoicePaymentMethod, -} from "../../../domain"; -import type { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; +} from "../../../../../domain"; +import type { + CustomerInvoiceCreationAttributes, + CustomerInvoiceModel, +} from "../../../../sequelize"; import { CustomerInvoiceItemDomainMapper } from "./customer-invoice-item.mapper"; import { CustomerInvoiceTaxesDomainMapper } from "./customer-invoice-taxes.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/index.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts index 40319a09..444296c5 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts @@ -15,8 +15,12 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import { type CustomerInvoice, type CustomerInvoiceProps, InvoiceRecipient } from "../../../domain"; -import type { CustomerInvoiceModel } from "../../sequelize"; +import { + type CustomerInvoice, + type CustomerInvoiceProps, + InvoiceRecipient, +} from "../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../sequelize"; export class InvoiceRecipientDomainMapper { public mapToDomain( diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-verifactu.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-verifactu.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts index 2d6a9d5c..6c692486 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-verifactu.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts @@ -16,8 +16,11 @@ import { type CustomerInvoiceProps, VerifactuRecord, VerifactuRecordEstado, -} from "../../../domain"; -import type { VerifactuRecordCreationAttributes, VerifactuRecordModel } from "../../sequelize"; +} from "../../../../../domain"; +import type { + VerifactuRecordCreationAttributes, + VerifactuRecordModel, +} from "../../../../sequelize"; export interface ICustomerInvoiceVerifactuDomainMapper extends ISequelizeDomainMapper< diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..9b0ff906 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./domain"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts index 624885c0..28104398 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts @@ -23,8 +23,8 @@ import { InvoiceAmount, type InvoiceRecipient, type VerifactuRecord, -} from "../../../domain"; -import type { CustomerInvoiceModel } from "../../sequelize"; +} from "../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../sequelize"; import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper"; import { VerifactuRecordListMapper } from "./verifactu-record.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/mappers/queries/index.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts index 4baaab39..f47025fa 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts @@ -17,8 +17,8 @@ import { } from "@repo/rdx-ddd"; import type { Result } from "@repo/rdx-utils"; -import { InvoiceRecipient } from "../../../domain"; -import type { CustomerInvoiceModel } from "../../sequelize"; +import { InvoiceRecipient } from "../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../sequelize"; import type { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts similarity index 95% rename from modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts index 9b053e60..fc98ec56 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/verifactu-record.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts @@ -13,8 +13,8 @@ import { } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { VerifactuRecord, VerifactuRecordEstado } from "../../../domain/"; -import type { VerifactuRecordModel } from "../../sequelize"; +import { VerifactuRecord, VerifactuRecordEstado } from "../../../../../domain"; +import type { VerifactuRecordModel } from "../../../../sequelize"; export interface IVerifactuRecordListMapper extends ISequelizeQueryMapper { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-criteria-whitelist.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-criteria-whitelist.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-item.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-item.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts similarity index 99% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts index 94ecd30f..9d4182e5 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts @@ -8,7 +8,7 @@ import { type Sequelize, } from "sequelize"; -import type { CustomerInvoice } from "../../../domain"; +import type { CustomerInvoice } from "../../../../domain"; export type CustomerInvoiceTaxCreationAttributes = InferCreationAttributes< CustomerInvoiceTaxModel, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/verifactu-record.model.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/verifactu-record.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/sequelize/models/verifactu-record.model.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/verifactu-record.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts index 6bde4a2d..cab13543 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts @@ -19,11 +19,12 @@ import type { ICustomerInvoiceDomainMapper, ICustomerInvoiceListMapper, } from "../mappers"; - -import { CustomerInvoiceModel } from "./models/customer-invoice.model"; -import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model"; -import { CustomerInvoiceTaxModel } from "./models/customer-invoice-tax.model"; -import { VerifactuRecordModel } from "./models/verifactu-record.model"; +import { + CustomerInvoiceItemModel, + CustomerInvoiceModel, + CustomerInvoiceTaxModel, + VerifactuRecordModel, +} from "../models"; export class CustomerInvoiceRepository extends SequelizeRepository diff --git a/modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts similarity index 90% rename from modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts rename to modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts index 9dbebc87..69ccf133 100644 --- a/modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts +++ b/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts @@ -6,8 +6,9 @@ import { CustomerInvoiceNumber, type CustomerInvoiceSerie, type ICustomerInvoiceNumberGenerator, -} from "../../domain"; -import { CustomerInvoiceModel } from "../sequelize"; +} from "../../../domain"; + +import { CustomerInvoiceModel } from "./models"; /** * Generador de números de factura @@ -44,12 +45,12 @@ export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGe lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta }); - let nextValue = "00001"; // valor inicial por defecto + let nextValue = "001"; // valor inicial por defecto if (lastInvoice) { const current = Number(lastInvoice.invoice_number); const next = Number.isFinite(current) && current > 0 ? current + 1 : 1; - nextValue = String(next).padStart(5, "0"); + nextValue = String(next).padStart(3, "0"); } const numberResult = CustomerInvoiceNumber.create(nextValue); diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/index.ts b/modules/customer-invoices/src/api/infrastructure/renderers/index.ts index 74a54f1a..e69de29b 100644 --- a/modules/customer-invoices/src/api/infrastructure/renderers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/renderers/index.ts @@ -1,2 +0,0 @@ -export * from "./issued-invoice-report-json.renderer"; -export * from "./issued-invoice-report-pdf.renderer"; diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts deleted file mode 100644 index dbf2d08f..00000000 --- a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice-report-pdf.renderer.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { DTO } from "@erp/core"; -import { FastReportRenderer } from "@erp/core/api"; -import { Result } from "@repo/rdx-utils"; - -export type IssuedInvoiceReportPDFRendererParams = { - companySlug: string; - documentId: string; -}; - -export class IssuedInvoiceReportPDFRenderer extends FastReportRenderer { - protected readonly templateName = "issued-invoice.frx"; - protected companySlug!: string; - - async render(source: DTO, params: IssuedInvoiceReportPDFRendererParams) { - this.companySlug = params.companySlug; - - const callResult = await this.renderInternal({ - inputData: source, - format: "PDF", - storageKey: { - documentType: "customer-invoice", - documentId: params.documentId, - format: "PDF", - }, - }); - - if (callResult.isFailure) { - return Result.fail(callResult.error); - } - - const { payload, templateChecksum } = callResult.data; - - return Result.ok({ - payload, - templateChecksum, - }); - } - - protected resolveTemplatePath(): string { - const templatePath = this.templateResolver.resolveTemplatePath( - "customer-invoices", - this.companySlug, - this.templateName - ); - - return templatePath; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts index 042f9ae5..e4fa0f96 100644 --- a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts @@ -3,7 +3,7 @@ import { Renderer } from "@erp/core/api"; import type { IssuedInvoiceFullRenderer, IssuedInvoiceReportRenderer, -} from "../../application/presenters"; +} from "../../application/snapshot-builders"; import type { CustomerInvoice } from "../../domain"; export class IssuedInvoiceReportJSONRenderer extends Renderer< diff --git a/modules/customer-invoices/src/api/infrastructure/services/index.ts b/modules/customer-invoices/src/api/infrastructure/services/index.ts deleted file mode 100644 index 04236857..00000000 --- a/modules/customer-invoices/src/api/infrastructure/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sequelize-invoice-number-generator"; diff --git a/modules/customers/src/api/application/index.ts b/modules/customers/src/api/application/index.ts index b17e4cff..2881f328 100644 --- a/modules/customers/src/api/application/index.ts +++ b/modules/customers/src/api/application/index.ts @@ -1,3 +1,3 @@ -export * from "./customer-application.service"; -export * from "./presenters"; +//export * from "./customer-application.service"; +//export * from "./presenters"; export * from "./use-cases"; diff --git a/modules/customers/src/api/index.ts b/modules/customers/src/api/index.ts index 8bde3a60..7f865797 100644 --- a/modules/customers/src/api/index.ts +++ b/modules/customers/src/api/index.ts @@ -1,5 +1,6 @@ -import { IModuleServer, ModuleParams } from "@erp/core/api"; -import { customersRouter, models } from "./infrastructure"; +import type { IModuleServer, ModuleParams } from "@erp/core/api"; + +import { models } from "./infrastructure"; export * from "./infrastructure/sequelize"; @@ -11,7 +12,7 @@ export const customersAPIModule: IModuleServer = { async init(params: ModuleParams) { // const contacts = getService("contacts"); const { logger } = params; - customersRouter(params); + //customersRouter(params); logger.info("🚀 Customers module initialized", { label: this.name }); }, async registerDependencies(params) { diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator index e221ce0c..1a54669a 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe index 0fb4b180..8bcd505b 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe differ