diff --git a/apps/server/package.json b/apps/server/package.json index f4bae231..06f81c2d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -45,6 +45,7 @@ "@erp/core": "workspace:*", "@erp/customer-invoices": "workspace:*", "@erp/customers": "workspace:*", + "@repo/rdx-logger": "workspace:*", "bcrypt": "^5.1.1", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", @@ -79,14 +80,9 @@ "node": ">=22" }, "tsup": { - "entry": [ - "src/index.ts" - ], + "entry": ["src/index.ts"], "outDir": "dist", - "format": [ - "esm", - "cjs" - ], + "format": ["esm", "cjs"], "target": "ES2022", "sourcemap": true, "clean": true, diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 7f4654e5..960a338c 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,8 +1,8 @@ -import { logger } from "@/lib/logger"; import cors, { CorsOptions } from "cors"; import express, { Application } from "express"; import helmet from "helmet"; import responseTime from "response-time"; +import { logger } from "./lib/logger"; // ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config. // dotenv.config(); diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts index 0fd4d7c2..e11a96a2 100644 --- a/apps/server/src/config/database.ts +++ b/apps/server/src/config/database.ts @@ -1,5 +1,5 @@ -import { logger } from "@/lib/logger"; import { Sequelize } from "sequelize"; +import { logger } from "../lib/logger"; import { ENV } from "./index"; /** diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index c520d919..8681f3d0 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,4 +1,3 @@ -import { logger } from "@/lib/logger"; import { DateTime } from "luxon"; import http from "node:http"; import os from "node:os"; @@ -7,7 +6,7 @@ import { createApp } from "./app"; import { ENV } from "./config"; import { tryConnectToDatabase } from "./config/database"; import { registerHealthRoutes } from "./health"; -import { listRoutes } from "./lib"; +import { listRoutes, logger } from "./lib"; import { initModules } from "./lib/modules"; import { registerModules } from "./register-modules"; diff --git a/apps/server/src/lib/logger.ts b/apps/server/src/lib/logger.ts new file mode 100644 index 00000000..a9bd8464 --- /dev/null +++ b/apps/server/src/lib/logger.ts @@ -0,0 +1,6 @@ +import { configureLogger, setLoggerSingleton } from "@repo/rdx-logger"; + +const logger = configureLogger("console"); // o según config/env +setLoggerSingleton(logger); + +export { logger }; diff --git a/apps/server/src/lib/logger/index.ts b/apps/server/src/lib/logger/index.ts deleted file mode 100644 index ca6bc5b2..00000000 --- a/apps/server/src/lib/logger/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ILogger } from "@erp/core/api"; -import { ConsoleLogger } from "./console-logger"; - -// Aquí podrías cambiar por SentryLogger en el futuro -const logger: ILogger = new ConsoleLogger(); - -export { logger }; -export type { ILogger }; diff --git a/modules/core/src/api/index.ts b/modules/core/src/api/index.ts index a7952f34..abc57b0f 100644 --- a/modules/core/src/api/index.ts +++ b/modules/core/src/api/index.ts @@ -2,5 +2,4 @@ export * from "./application"; export * from "./domain"; export * from "./helpers"; export * from "./infrastructure"; -export * from "./logger"; export * from "./modules"; diff --git a/modules/core/src/api/logger/index.ts b/modules/core/src/api/logger/index.ts deleted file mode 100644 index cdb53790..00000000 --- a/modules/core/src/api/logger/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./logger.interface"; diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index d5b2cda6..20216039 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -41,6 +41,7 @@ "@hookform/resolvers": "^5.0.1", "@repo/rdx-criteria": "workspace:*", "@repo/rdx-ddd": "workspace:*", + "@repo/rdx-logger": "workspace:*", "@repo/rdx-ui": "workspace:*", "@repo/rdx-utils": "workspace:*", "@repo/shadcn-ui": "workspace:*", diff --git a/modules/customers/package.json b/modules/customers/package.json index 281d9a52..c174b44f 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -34,6 +34,7 @@ "@hookform/resolvers": "^5.0.1", "@repo/rdx-criteria": "workspace:*", "@repo/rdx-ddd": "workspace:*", + "@repo/rdx-logger": "workspace:*", "@repo/rdx-ui": "workspace:*", "@repo/rdx-utils": "workspace:*", "@repo/shadcn-ui": "workspace:*", diff --git a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts index 593550f3..486d7a74 100644 --- a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts +++ b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts @@ -1,5 +1,6 @@ import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd"; import { CustomerService } from "../../domain"; +import { logger } from "../../helpers"; export class CustomerNotExistsInCompanySpecification extends CompositeSpecification { constructor( @@ -11,11 +12,24 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat } public async isSatisfiedBy(customerId: UniqueID): Promise { - const exists = await this.service.existsByIdInCompany( - customerId, + const existsCheck = await this.service.existsByIdInCompany( this.companyId, + customerId, this.transaction ); - return !exists; + + if (existsCheck.isFailure) { + throw existsCheck.error; + } + + const customerExists = existsCheck.data; + logger.debug( + `customerExists => ${customerExists}, ${JSON.stringify({ customerId, companyId: this.companyId }, null, 2)}`, + { + label: "CustomerNotExistsInCompanySpecification", + } + ); + + return customerExists === false; } } diff --git a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts b/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts index 900d24ab..9415fab3 100644 --- a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts @@ -3,6 +3,7 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CreateCustomerRequestDTO } from "../../../../common"; +import { logger } from "../../..//helpers"; import { CustomerService } from "../../../domain"; import { CustomerFullPresenter } from "../../presenters"; import { CustomerNotExistsInCompanySpecification } from "../../specs"; @@ -52,12 +53,16 @@ export class CreateCustomerUseCase { companyId, transaction ); + const isNew = await spec.isSatisfiedBy(newCustomer.id); + logger.debug(`isNew => ${isNew}`, { label: "CreateCustomerUseCase.execute" }); if (!isNew) { return Result.fail(new DuplicateEntityError("Customer", "id", String(newCustomer.id))); } + logger.debug(JSON.stringify(newCustomer, null, 6)); + const saveResult = await this.service.saveCustomer(newCustomer, transaction); if (saveResult.isFailure) { return Result.fail(saveResult.error); diff --git a/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts b/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts index 59edf331..98f476c2 100644 --- a/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts +++ b/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts @@ -40,10 +40,10 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) { const errors: ValidationErrorDetail[] = []; const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors); + const status = CustomerStatus.createActive(); const isCompany = dto.is_company === "true"; - const status = extractOrPushError(CustomerStatus.create(dto.status), "status", errors); + const reference = extractOrPushError( maybeFromNullableVO(dto.reference, (value) => Name.create(value)), "reference", @@ -100,15 +100,39 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) { errors ); - const emailAddress = extractOrPushError( - maybeFromNullableVO(dto.email, (value) => EmailAddress.create(value)), - "email", + const primaryEmailAddress = extractOrPushError( + maybeFromNullableVO(dto.email_primary, (value) => EmailAddress.create(value)), + "email_primary", errors ); - const phoneNumber = extractOrPushError( - maybeFromNullableVO(dto.phone, (value) => PhoneNumber.create(value)), - "phone", + const secondaryEmailAddress = extractOrPushError( + maybeFromNullableVO(dto.email_secondary, (value) => EmailAddress.create(value)), + "email_secondary", + errors + ); + + const primaryPhoneNumber = extractOrPushError( + maybeFromNullableVO(dto.phone_primary, (value) => PhoneNumber.create(value)), + "phone_primary", + errors + ); + + const secondaryPhoneNumber = extractOrPushError( + maybeFromNullableVO(dto.phone_secondary, (value) => PhoneNumber.create(value)), + "phone_secondary", + errors + ); + + const primaryMobileNumber = extractOrPushError( + maybeFromNullableVO(dto.mobile_primary, (value) => PhoneNumber.create(value)), + "mobile_primary", + errors + ); + + const secondaryMobileNumber = extractOrPushError( + maybeFromNullableVO(dto.mobile_secondary, (value) => PhoneNumber.create(value)), + "mobile_secondary", errors ); @@ -145,7 +169,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) { const defaultTaxes = new Collection(); if (!isNullishOrEmpty(dto.default_taxes)) { - dto.default_taxes.split(",").map((taxCode, index) => { + dto.default_taxes!.map((taxCode, index) => { const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); if (tax) { defaultTaxes.add(tax!); @@ -172,8 +196,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) { errors ); - const customerProps: CustomerProps = { - companyId: companyId!, + const customerProps: Omit = { status: status!, reference: reference!, @@ -184,8 +207,13 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) { address: postalAddress!, - email: emailAddress!, - phone: phoneNumber!, + emailPrimary: primaryEmailAddress!, + emailSecondary: secondaryEmailAddress!, + phonePrimary: primaryPhoneNumber!, + phoneSecondary: secondaryPhoneNumber!, + mobilePrimary: primaryMobileNumber!, + mobileSecondary: secondaryMobileNumber!, + fax: faxNumber!, website: website!, diff --git a/modules/customers/src/api/helpers/index.ts b/modules/customers/src/api/helpers/index.ts index 51a58083..41c7bf27 100644 --- a/modules/customers/src/api/helpers/index.ts +++ b/modules/customers/src/api/helpers/index.ts @@ -1 +1 @@ -export * from "../application/create-customer/map-dto-to-create-customer-props"; +export * from "./logger"; diff --git a/modules/customers/src/api/helpers/logger.ts b/modules/customers/src/api/helpers/logger.ts new file mode 100644 index 00000000..dff536f0 --- /dev/null +++ b/modules/customers/src/api/helpers/logger.ts @@ -0,0 +1,3 @@ +import { loggerSingleton } from "@repo/rdx-logger"; + +export const logger = loggerSingleton(); diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 28e8c805..1841ee41 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -32,7 +32,7 @@ export type CustomerDeps = { }; export function buildCustomerDependencies(params: ModuleParams): CustomerDeps { - const { database } = params; + const { database, logger } = params; const transactionManager = new SequelizeTransactionManager(database); // Mapper Registry diff --git a/modules/customers/src/common/dto/request/create-customer.request.dto.ts b/modules/customers/src/common/dto/request/create-customer.request.dto.ts index 47336c2c..09e9f6e6 100644 --- a/modules/customers/src/common/dto/request/create-customer.request.dto.ts +++ b/modules/customers/src/common/dto/request/create-customer.request.dto.ts @@ -1,9 +1,10 @@ import * as z from "zod/v4"; export const CreateCustomerRequestSchema = z.object({ - reference: z.string().optional(), + id: z.string().nonempty(), - is_company: z.string().toLowerCase().default("false").optional(), + reference: z.string().optional(), + is_company: z.string().toLowerCase().default("false"), name: z.string(), trade_name: z.string().optional(), tin: z.string().optional(), @@ -28,8 +29,8 @@ export const CreateCustomerRequestSchema = z.object({ legal_record: z.string().optional(), - language_code: z.string().toLowerCase().default("es").optional(), - currency_code: z.string().toUpperCase().default("EUR").optional(), + language_code: z.string().toLowerCase().default("es"), + currency_code: z.string().toUpperCase().default("EUR"), }); export type CreateCustomerRequestDTO = z.infer; diff --git a/modules/verifactu/src/api/application/use-cases/index.ts b/modules/verifactu/src/api/application/use-cases/index.ts new file mode 100644 index 00000000..b6dd097f --- /dev/null +++ b/modules/verifactu/src/api/application/use-cases/index.ts @@ -0,0 +1 @@ +export * from "./send-invoice-verifactu.use-case"; diff --git a/modules/verifactu/src/api/application/use-cases/send/index.ts b/modules/verifactu/src/api/application/use-cases/send/index.ts new file mode 100644 index 00000000..b6dd097f --- /dev/null +++ b/modules/verifactu/src/api/application/use-cases/send/index.ts @@ -0,0 +1 @@ +export * from "./send-invoice-verifactu.use-case"; diff --git a/modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts b/modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts new file mode 100644 index 00000000..36903c29 --- /dev/null +++ b/modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts @@ -0,0 +1,41 @@ +import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { VerifactuRecordService } from "../../../domain/services/verifactu-record.service"; + +type SendInvoiceVerifactuUseCaseInput = { + invoice_id: string; +}; + +export class SendInvoiceVerifactuUseCase { + constructor( + private readonly service: VerifactuRecordService, + private readonly transactionManager: ITransactionManager, + private readonly presenterRegistry: IPresenterRegistry + ) {} + + public async execute(params: SendInvoiceVerifactuUseCaseInput) { + const { invoice_id } = 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 invoiceOrError = await this.service.sendInvoiceToVerifactu(invoiceId, transaction); + if (invoiceOrError.isFailure) { + return Result.fail(invoiceOrError.error); + } + + const invoice = invoiceOrError.data; + return Result.ok({}); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/verifactu/src/api/domain/aggregates/index.ts b/modules/verifactu/src/api/domain/aggregates/index.ts new file mode 100644 index 00000000..93badda2 --- /dev/null +++ b/modules/verifactu/src/api/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./verifactu-record"; diff --git a/modules/verifactu/src/api/domain/aggregates/verifactu-record.ts b/modules/verifactu/src/api/domain/aggregates/verifactu-record.ts new file mode 100644 index 00000000..13933d03 --- /dev/null +++ b/modules/verifactu/src/api/domain/aggregates/verifactu-record.ts @@ -0,0 +1,42 @@ +import { AggregateRoot, UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +export interface VerifactuRecordProps { + id: UniqueID; + invoiceId: UniqueID; + estado: string; + url: string; + qr1: JSON; + qr2: Blob; +} + +export type IVerifactuRecord = {}; + +export class VerifactuRecord + extends AggregateRoot + implements IVerifactuRecord +{ + protected constructor(props: VerifactuRecordProps, invoiceId: UniqueID) { + super(props, invoiceId); + } + + static create(props: VerifactuRecordProps, invoiceId: UniqueID): Result { + const verifactuRecord = new VerifactuRecord(props, invoiceId); + + // Reglas de negocio / validaciones + + return Result.ok(verifactuRecord); + } + + public get invoiceId(): UniqueID { + return this.props.invoiceId; + } + + public get estado(): string { + return this.props.estado; + } + + public get url(): string { + return this.props.url; + } +} diff --git a/modules/verifactu/src/api/domain/repositories/verifactu-repository.interface.ts b/modules/verifactu/src/api/domain/repositories/verifactu-repository.interface.ts index a266bfbb..aedb18d1 100644 --- a/modules/verifactu/src/api/domain/repositories/verifactu-repository.interface.ts +++ b/modules/verifactu/src/api/domain/repositories/verifactu-repository.interface.ts @@ -1,75 +1,26 @@ -import { Criteria } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceListDTO } from "../../infrastructure"; -import { CustomerInvoice } from "../aggregates"; +import { UniqueID } from "../../../../../../packages/rdx-ddd/src"; +import { Result } from "../../../../../../packages/rdx-utils/src"; +import { VerifactuRecord } from "../aggregates"; /** - * Interfaz del repositorio para el agregado `CustomerInvoice`. + * Interfaz del repositorio para el agregado `Verifactu`. * El escopado multitenant está representado por `companyId`. */ -export interface ICustomerInvoiceRepository { +export interface IVerifactuRecordRepository { /** * * Persiste una nueva factura o actualiza una existente. * Retorna el objeto actualizado tras la operación. * - * @param invoice - El agregado a guardar. + * @param verifactuRecord - El agregado a guardar. * @param transaction - Transacción activa para la operación. - * @returns Result + * @returns Result */ - save(invoice: CustomerInvoice, transaction: any): Promise>; + save(verifactuRecord: VerifactuRecord, transaction: any): Promise>; /** - * Comprueba si existe una factura con un `id` dentro de una `company`. - */ - existsByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction?: any - ): Promise>; - - /** - * Recupera una factura por su ID y companyId. + * Recupera un registro por su ID. * Devuelve un `NotFoundError` si no se encuentra. */ - getByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction?: any - ): Promise>; - - /** - * - * Consulta facturas dentro de una empresa usando un - * objeto Criteria (filtros, orden, paginación). - * El resultado está encapsulado en un objeto `Collection`. - * - * @param companyId - ID de la empresa. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result, Error> - * - * @see Criteria - */ - findByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: any - ): Promise, Error>>; - - /** - * - * Elimina o marca como eliminada una factura dentro de una empresa. - * - * @param companyId - ID de la empresa. - * @param id - UUID de la factura a eliminar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - deleteByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: any - ): Promise>; + getById(id: UniqueID, transaction?: any): Promise>; } diff --git a/modules/verifactu/src/api/domain/services/verifactu-record.service.ts b/modules/verifactu/src/api/domain/services/verifactu-record.service.ts new file mode 100644 index 00000000..01743c72 --- /dev/null +++ b/modules/verifactu/src/api/domain/services/verifactu-record.service.ts @@ -0,0 +1,72 @@ +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { Transaction } from "sequelize"; +import { CustomerInvoice, VerifactuRecord } from "../aggregates"; +import { IVerifactuRecordRepository } from "../repositories/verifactu-repository.interface"; + +export class VerifactuRecordService { + constructor(private readonly repository: IVerifactuRecordRepository) {} + + /** + * Guarda un registro de VerifactuRecord en persistencia. + * + * @param verifactuRecord - El agregado a guardar. + * @param transaction - Transacción activa para la operación. + * @returns Result - El agregado guardado o un error si falla la operación. + */ + async saveVerifactuRecord( + verifactuRecord: VerifactuRecord, + transaction: any + ): Promise> { + return this.repository.save(verifactuRecord, transaction); + } + + /** + * Actualiza parcialmente una factura existente con nuevos datos. + * No lo guarda en el repositorio. + * + * @param companyId - Identificador de la empresa a la que pertenece el cliente. + * @param invoiceId - Identificador de la factura a actualizar. + * @param changes - Subconjunto de props válidas para aplicar. + * @param transaction - Transacción activa para la operación. + * @returns Result - Factura actualizada o error. + */ + async sendInvoiceToVerifactu( + invoiceId: UniqueID, + transaction?: Transaction + ): Promise> { + // Verificar si la factura existe + // const invoiceResult = await this.getInvoiceByIdInCompany(invoiceId, transaction); + + // if (invoiceResult.isFailure) { + // return Result.fail(invoiceResult.error); + // } + + const invoice = { + serie: "A", + numero: "1", + fecha_expedicion: "12-09-2025", + tipo_factura: "F1", + descripcion: "Venta de bienes", + nif: "A15022510", + nombre: "Empresa de prueba SL", + lineas: [ + { + base_imponible: "200", + tipo_impositivo: "21", + cuota_repercutida: "42", + }, + { + base_imponible: "100", + tipo_impositivo: "10", + cuota_repercutida: "10", + }, + ], + importe_total: "352.00", + }; + + console.log("ESTO ES UNA PRUEBA>>>>>>"); + + return Result.ok(invoice); + } +} diff --git a/modules/verifactu/src/api/domain/services/verifactu.service.ts b/modules/verifactu/src/api/domain/services/verifactu.service.ts deleted file mode 100644 index a0fa7535..00000000 --- a/modules/verifactu/src/api/domain/services/verifactu.service.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Criteria } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { Transaction } from "sequelize"; -import { CustomerInvoiceListDTO } from "../../infrastructure"; -import { CustomerInvoice, CustomerInvoicePatchProps, CustomerInvoiceProps } from "../aggregates"; -import { ICustomerInvoiceRepository } from "../repositories"; - -export class CustomerInvoiceService { - constructor(private readonly repository: ICustomerInvoiceRepository) {} - - /** - * Construye un nuevo agregado CustomerInvoice a partir de props validadas. - * - * @param companyId - Identificador de la empresa a la que pertenece el cliente. - * @param props - Las propiedades ya validadas para crear la factura. - * @param invoiceId - Identificador UUID de la factura (opcional). - * @returns Result - El agregado construido o un error si falla la creación. - */ - buildInvoiceInCompany( - companyId: UniqueID, - props: Omit, - invoiceId?: UniqueID - ): Result { - return CustomerInvoice.create({ ...props, companyId }, invoiceId); - } - - /** - * Guarda una instancia de CustomerInvoice en persistencia. - * - * @param invoice - El agregado a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - El agregado guardado o un error si falla la operación. - */ - async saveInvoice( - invoice: CustomerInvoice, - transaction: any - ): Promise> { - return this.repository.save(invoice, transaction); - } - - /** - * - * Comprueba si existe o no en persistencia una factura con el ID proporcionado - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param invoiceId - Identificador UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @returns Result - Existe la factura o no. - */ - - async existsByIdInCompany( - companyId: UniqueID, - invoiceId: UniqueID, - transaction?: any - ): Promise> { - return this.repository.existsByIdInCompany(companyId, invoiceId, transaction); - } - - /** - * Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Objeto con condiciones de filtro, paginación y orden. - * @param transaction - Transacción activa para la operación. - * @returns Result, Error> - Colección de facturas o error. - */ - async findInvoiceByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction?: Transaction - ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); - } - - /** - * Recupera una factura por su identificador único. - * - * @param invoiceId - Identificador UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @returns Result - Factura encontrada o error. - */ - async getInvoiceByIdInCompany( - companyId: UniqueID, - invoiceId: UniqueID, - transaction?: Transaction - ): Promise> { - return await this.repository.getByIdInCompany(companyId, invoiceId, transaction); - } - - /** - * Actualiza parcialmente una factura existente con nuevos datos. - * No lo guarda en el repositorio. - * - * @param companyId - Identificador de la empresa a la que pertenece el cliente. - * @param invoiceId - Identificador de la factura a actualizar. - * @param changes - Subconjunto de props válidas para aplicar. - * @param transaction - Transacción activa para la operación. - * @returns Result - Factura actualizada o error. - */ - async updateInvoiceByIdInCompany( - companyId: UniqueID, - invoiceId: UniqueID, - changes: CustomerInvoicePatchProps, - transaction?: Transaction - ): Promise> { - // Verificar si la factura existe - const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction); - - if (invoiceResult.isFailure) { - return Result.fail(invoiceResult.error); - } - - const invoice = invoiceResult.data; - const updatedInvoice = invoice.update(changes); - - if (updatedInvoice.isFailure) { - return Result.fail(updatedInvoice.error); - } - - return Result.ok(updatedInvoice.data); - } - - /** - * Elimina (o marca como eliminada) una factura según su ID. - * - * @param companyId - Identificador de la empresa a la que pertenece el cliente. - * @param invoiceId - Identificador UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @returns Result - Resultado de la operación. - */ - async deleteInvoiceByIdInCompany( - companyId: UniqueID, - invoiceId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction); - } -} diff --git a/modules/verifactu/src/api/infrastructure/express/controllers/index.ts b/modules/verifactu/src/api/infrastructure/express/controllers/index.ts new file mode 100644 index 00000000..e0ead65d --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/express/controllers/index.ts @@ -0,0 +1 @@ +export * from "./send-invoice-verifactu.controller"; diff --git a/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts b/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts new file mode 100644 index 00000000..2975414a --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts @@ -0,0 +1,25 @@ +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { SendInvoiceVerifactuUseCase } from "../../../application/use-cases/send"; + +export class SendInvoiceVerifactuController extends ExpressController { + public constructor(private readonly useCase: SendInvoiceVerifactuUseCase) { + super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + const { invoice_id } = this.req.params; + + const result = await this.useCase.execute({ invoice_id, companyId }); + + return result.match( + ({ data, filename }) => this.downloadPDF(data, filename), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts b/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts new file mode 100644 index 00000000..6b4ead59 --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts @@ -0,0 +1,48 @@ +import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; +import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; +import { Application, NextFunction, Request, Response, Router } from "express"; +import { Sequelize } from "sequelize"; +import { ReportCustomerInvoiceByIdRequestSchema } from "../../../common/dto"; +import { SendInvoiceVerifactuController } from "./controllers"; + +export const verifactuRouter = (params: ModuleParams) => { + const { app, baseRoutePath, logger } = params as { + app: Application; + database: Sequelize; + baseRoutePath: string; + logger: ILogger; + }; + + //const deps = buildCustomerInvoiceDependencies(params); + + const router: Router = Router({ mergeParams: true }); + + // 🔐 Autenticación + Tenancy para TODO el router + if (process.env.NODE_ENV === "development") { + router.use( + (req: Request, res: Response, next: NextFunction) => + mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas + ); + } + + router.use([ + (req: Request, res: Response, next: NextFunction) => + enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + + (req: Request, res: Response, next: NextFunction) => + enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas + ]); + + // ---------------------------------------------- + + router.get( + "/:invoice_id/sendVerifactu", + //checkTabContext, + validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"), + (req: Request, res: Response, next: NextFunction) => { + const useCase = deps.build.report(); + const controller = new SendInvoiceVerifactuController(useCase); + return controller.execute(req, res, next); + } + ); +}; diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json new file mode 100644 index 00000000..217c6811 --- /dev/null +++ b/packages/rdx-logger/package.json @@ -0,0 +1,20 @@ +{ + "name": "@repo/rdx-logger", + "version": "0.0.1", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "clean": "rm -rf node_modules" + }, + "exports": { + ".": "./src/index.ts" + }, + "devDependencies": { + "typescript": "^5.8.3" + }, + "dependencies": { + "cls-rtracer": "^2.6.3", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + } +} diff --git a/packages/rdx-logger/src/configure-logger.ts b/packages/rdx-logger/src/configure-logger.ts new file mode 100644 index 00000000..d6cf189c --- /dev/null +++ b/packages/rdx-logger/src/configure-logger.ts @@ -0,0 +1,17 @@ +import { createConsoleLogger, createSentryLogger, createWinstonLogger } from "./strategies"; +import type { ILogger } from "./types"; + +export type LoggerStrategy = "console" | "winston" | "sentry"; + +export function configureLogger(strategy: LoggerStrategy = "console"): ILogger { + switch (strategy) { + case "console": + return createConsoleLogger(); + case "winston": + return createWinstonLogger(); + case "sentry": + return createSentryLogger(); + default: + throw new Error(`Unknown logger strategy: ${strategy}`); + } +} diff --git a/packages/rdx-logger/src/index.ts b/packages/rdx-logger/src/index.ts new file mode 100644 index 00000000..99d28063 --- /dev/null +++ b/packages/rdx-logger/src/index.ts @@ -0,0 +1,3 @@ +export { configureLogger } from "./configure-logger"; +export { loggerSingleton, setLoggerSingleton } from "./singleton"; +export * from "./types"; diff --git a/packages/rdx-logger/src/singleton.ts b/packages/rdx-logger/src/singleton.ts new file mode 100644 index 00000000..010f19b6 --- /dev/null +++ b/packages/rdx-logger/src/singleton.ts @@ -0,0 +1,15 @@ +import { configureLogger } from "./configure-logger"; +import type { ILogger } from "./types"; + +let _loggerInstance: ILogger | null = null; + +export const loggerSingleton = (): ILogger => { + if (!_loggerInstance) { + _loggerInstance = configureLogger("console"); // o desde process.env + } + return _loggerInstance; +}; + +export const setLoggerSingleton = (logger: ILogger): void => { + _loggerInstance = logger; +}; diff --git a/apps/server/src/lib/logger/console-logger.ts b/packages/rdx-logger/src/strategies/console-logger.ts similarity index 80% rename from apps/server/src/lib/logger/console-logger.ts rename to packages/rdx-logger/src/strategies/console-logger.ts index 39cade34..5576037e 100644 --- a/apps/server/src/lib/logger/console-logger.ts +++ b/packages/rdx-logger/src/strategies/console-logger.ts @@ -1,4 +1,4 @@ -import { ILogger } from "@erp/core/api"; +import { ILogger } from "../types"; export class ConsoleLogger implements ILogger { info(message: string, meta?: any) { @@ -17,3 +17,5 @@ export class ConsoleLogger implements ILogger { console.debug(`[DEBUG] ${message}`, meta ?? ""); } } + +export const createConsoleLogger = (): ILogger => new ConsoleLogger(); diff --git a/packages/rdx-logger/src/strategies/index.ts b/packages/rdx-logger/src/strategies/index.ts new file mode 100644 index 00000000..5698748d --- /dev/null +++ b/packages/rdx-logger/src/strategies/index.ts @@ -0,0 +1,3 @@ +export * from "./console-logger"; +export * from "./sentry-logger"; +export * from "./winston-logger"; diff --git a/apps/server/src/lib/logger/sentry-logger.ts b/packages/rdx-logger/src/strategies/sentry-logger.ts similarity index 86% rename from apps/server/src/lib/logger/sentry-logger.ts rename to packages/rdx-logger/src/strategies/sentry-logger.ts index dc149c01..138c83b7 100644 --- a/apps/server/src/lib/logger/sentry-logger.ts +++ b/packages/rdx-logger/src/strategies/sentry-logger.ts @@ -1,4 +1,4 @@ -import { ILogger } from "@erp/core/api"; +import { ILogger } from "../types"; export class SentryLogger implements ILogger { // biome-ignore lint/complexity/noUselessConstructor: @@ -25,3 +25,5 @@ export class SentryLogger implements ILogger { //Sentry.captureMessage(message, "debug"); } } + +export const createSentryLogger = (): ILogger => new SentryLogger(); diff --git a/apps/server/src/lib/logger/winston-logger.ts b/packages/rdx-logger/src/strategies/winston-logger.ts similarity index 93% rename from apps/server/src/lib/logger/winston-logger.ts rename to packages/rdx-logger/src/strategies/winston-logger.ts index 8ef487c3..7522aa8f 100644 --- a/apps/server/src/lib/logger/winston-logger.ts +++ b/packages/rdx-logger/src/strategies/winston-logger.ts @@ -1,6 +1,6 @@ -import { ILogger } from "@erp/core/api"; import rTracer from "cls-rtracer"; import { createLogger, format, transports } from "winston"; +import { ILogger } from "../types"; const winston = createLogger({ level: "info", @@ -61,3 +61,5 @@ export class WinstonLogger implements ILogger { winston.error(message, { error: error?.stack || error }); } } + +export const createWinstonLogger = (): ILogger => new WinstonLogger(); diff --git a/modules/core/src/api/logger/logger.interface.ts b/packages/rdx-logger/src/types.ts similarity index 100% rename from modules/core/src/api/logger/logger.interface.ts rename to packages/rdx-logger/src/types.ts diff --git a/packages/rdx-logger/tsconfig.json b/packages/rdx-logger/tsconfig.json new file mode 100644 index 00000000..a625a4d0 --- /dev/null +++ b/packages/rdx-logger/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true + }, + "include": ["src", "../../modules/core/src/web/lib/helpers/money-funcs.ts"], + "exclude": ["src/**/__tests__/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 396e0a7c..c13272ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@erp/customers': specifier: workspace:* version: link:../../modules/customers + '@repo/rdx-logger': + specifier: workspace:* + version: link:../../packages/rdx-logger bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -465,6 +468,9 @@ importers: '@repo/rdx-ddd': specifier: workspace:* version: link:../../packages/rdx-ddd + '@repo/rdx-logger': + specifier: workspace:* + version: link:../../packages/rdx-logger '@repo/rdx-ui': specifier: workspace:* version: link:../../packages/rdx-ui @@ -574,6 +580,9 @@ importers: '@repo/rdx-ddd': specifier: workspace:* version: link:../../packages/rdx-ddd + '@repo/rdx-logger': + specifier: workspace:* + version: link:../../packages/rdx-logger '@repo/rdx-ui': specifier: workspace:* version: link:../../packages/rdx-ui @@ -692,6 +701,22 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/rdx-logger: + dependencies: + cls-rtracer: + specifier: ^2.6.3 + version: 2.6.3 + winston: + specifier: ^3.17.0 + version: 3.17.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.17.0) + devDependencies: + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/rdx-ui: dependencies: '@dnd-kit/core':