From 4b93815985679678df9b74e607af9d01edbc9032 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 25 Aug 2025 19:42:56 +0200 Subject: [PATCH] Facturas de cliente y clientes --- .../application/create-account.use-case.ts | 2 +- .../application/update-account.use-case.ts | 4 +- .../account-service.integration.test.ts | 4 +- .../domain/services/account-service.test.ts | 2 +- .../infraestructure/mappers/account.mapper.ts | 4 +- .../sequelize/account.model.ts | 4 +- .../create-account.presenter.ts | 2 +- .../get-account/get-account.presenter.ts | 2 +- .../list-accounts/list-accounts.presenter.ts | 2 +- .../update-account.presenter.ts | 2 +- .../presentation/dto/accounts.request.dto.ts | 6 +- .../presentation/dto/accounts.response.dto.ts | 8 +- .../presentation/dto/accounts.schemas.ts | 4 +- .../infraestructure/mappers/contact.mapper.ts | 4 +- .../sequelize/contact.model.ts | 4 +- .../list/list-contacts.presenter.ts | 2 +- .../presentation/dto/contacts.response.dto.ts | 2 +- .../sequelize/customer.model.ts | 4 +- .../list/list-customer-invoices.presenter.ts | 2 +- .../customer-invoices.response.dto.ts | 4 +- .../dto/customers.response.dto.ts | 2 +- apps/server/package.json | 2 +- apps/web/src/app.tsx | 2 +- apps/web/tsconfig.app.json | 4 +- .../presentation/dto/invoices.request.dto.ts | 4 +- modules/auth/tsconfig.json | 4 +- .../application/errors/application-error.ts | 12 ++ .../core/src/api/application/errors/index.ts | 1 + modules/core/src/api/application/index.ts | 1 + .../src/api/domain/errors/domain-error.ts | 12 ++ .../domain/errors/domain-validation-error.ts | 52 +++++ .../domain/errors/duplicate-entity-error.ts | 11 + .../domain/errors/entity-not-found-error.ts | 11 + modules/core/src/api/domain/errors/index.ts | 5 + .../errors/validation-error-collection.ts | 19 +- modules/core/src/api/domain/index.ts | 1 + .../src/api/errors/domain-validation-error.ts | 29 --- .../src/api/errors/duplicate-entity-error.ts | 6 - .../src/api/errors/entity-not-found-error.ts | 6 - modules/core/src/api/errors/error-mapper.ts | 136 ------------- modules/core/src/api/index.ts | 3 +- .../src/api/infrastructure/errors/index.ts | 3 + .../errors/infrastructure-errors.ts | 11 + .../errors/infrastructure-repository-error.ts | 11 + .../infrastructure-unavailable-error.ts | 11 + .../express/api-error-mapper.ts | 188 ++++++++++++++++++ .../express}/errors/api-error.ts | 4 +- .../express}/errors/conflict-api-error.ts | 2 +- .../express}/errors/forbidden-api-error.ts | 2 +- .../express}/errors/index.ts | 5 - .../express}/errors/internal-api-error.ts | 2 +- .../express}/errors/not-found-api-error.ts | 2 +- .../express}/errors/unauthorized-api-error.ts | 2 +- .../express}/errors/unavailable-api-error.ts | 2 +- .../express}/errors/validation-api-error.ts | 2 +- .../express/express-controller.ts | 2 +- .../infrastructure/express/express-guards.ts | 2 +- .../src/api/infrastructure/express/index.ts | 2 + .../middlewares/global-error-handler.ts | 16 +- .../express/middlewares/validate-request.ts | 2 +- modules/core/src/api/infrastructure/index.ts | 1 + .../src/api/infrastructure/sequelize/index.ts | 1 + .../sequelize/sequelize-error-translator.ts | 77 +++++++ modules/core/tsconfig.json | 4 +- .../delete-customer-invoice.use-case.ts | 2 +- ...customer-invoice-id-already-exits-error.ts | 9 + .../src/api/domain/errors/index.ts | 1 + .../customer-invoices/src/api/domain/index.ts | 1 + .../customer-invoices-api-error-mapper.ts | 21 ++ modules/customer-invoices/tsconfig.json | 4 +- .../create-customer.use-case.ts | 11 +- .../delete-customer.use-case.ts | 7 +- .../assembler/get-customer.assembler.ts | 2 +- .../get-customer/get-customer.use-case.ts | 24 ++- .../assembler/list-customers.assembler.ts | 2 +- .../src/api/domain/aggregates/customer.ts | 25 ++- .../customer-repository.interface.ts | 62 +++--- .../services/customer-service.interface.ts | 61 ++++-- .../api/domain/services/customer.service.ts | 159 +++++++-------- .../src/api/infrastructure/dependencies.ts | 4 +- .../controllers/create-customer.controller.ts | 2 +- .../express/customers.routes.ts | 3 +- .../sequelize/customer.model.ts | 19 +- .../sequelize/customer.repository.ts | 111 +++++++---- .../request/create-customer.request.dto.ts | 6 +- .../response/customer-creation.result.dto.ts | 2 +- .../response/customer-list.response.dto.ts | 2 +- .../get-customer-by-id.response.dto.ts | 2 +- .../customers/src/web/pages/create/create.tsx | 18 +- .../web/pages/create/customer-edit-form.tsx | 104 ++++++---- modules/customers/tsconfig.json | 4 +- packages.bak/rdx-verifactu/tsconfig.json | 2 +- .../src/components/layout/app-sidebar.tsx | 14 +- .../layout/chart-area-interactive.tsx | 2 +- 94 files changed, 930 insertions(+), 501 deletions(-) create mode 100644 modules/core/src/api/application/errors/application-error.ts create mode 100644 modules/core/src/api/application/errors/index.ts create mode 100644 modules/core/src/api/application/index.ts create mode 100644 modules/core/src/api/domain/errors/domain-error.ts create mode 100644 modules/core/src/api/domain/errors/domain-validation-error.ts create mode 100644 modules/core/src/api/domain/errors/duplicate-entity-error.ts create mode 100644 modules/core/src/api/domain/errors/entity-not-found-error.ts create mode 100644 modules/core/src/api/domain/errors/index.ts rename modules/core/src/api/{ => domain}/errors/validation-error-collection.ts (65%) create mode 100644 modules/core/src/api/domain/index.ts delete mode 100644 modules/core/src/api/errors/domain-validation-error.ts delete mode 100644 modules/core/src/api/errors/duplicate-entity-error.ts delete mode 100644 modules/core/src/api/errors/entity-not-found-error.ts delete mode 100644 modules/core/src/api/errors/error-mapper.ts create mode 100644 modules/core/src/api/infrastructure/errors/index.ts create mode 100644 modules/core/src/api/infrastructure/errors/infrastructure-errors.ts create mode 100644 modules/core/src/api/infrastructure/errors/infrastructure-repository-error.ts create mode 100644 modules/core/src/api/infrastructure/errors/infrastructure-unavailable-error.ts create mode 100644 modules/core/src/api/infrastructure/express/api-error-mapper.ts rename modules/core/src/api/{ => infrastructure/express}/errors/api-error.ts (89%) rename modules/core/src/api/{ => infrastructure/express}/errors/conflict-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/forbidden-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/index.ts (59%) rename modules/core/src/api/{ => infrastructure/express}/errors/internal-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/not-found-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/unauthorized-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/unavailable-api-error.ts (99%) rename modules/core/src/api/{ => infrastructure/express}/errors/validation-api-error.ts (99%) create mode 100644 modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts create mode 100644 modules/customer-invoices/src/api/domain/errors/customer-invoice-id-already-exits-error.ts create mode 100644 modules/customer-invoices/src/api/domain/errors/index.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts diff --git a/apps/server/archive/contexts/accounts/application/create-account.use-case.ts b/apps/server/archive/contexts/accounts/application/create-account.use-case.ts index 87d35d7a..4c01fdf7 100644 --- a/apps/server/archive/contexts/accounts/application/create-account.use-case.ts +++ b/apps/server/archive/contexts/accounts/application/create-account.use-case.ts @@ -74,7 +74,7 @@ export class CreateAccountUseCase { const validatedData: IAccountProps = { status: AccountStatus.createInactive(), - isFreelancer: dto.is_freelancer, + isFreelancer: dto.is_companyr, name: dto.name, tradeName: dto.trade_name ? Maybe.some(dto.trade_name) : Maybe.none(), tin: tinOrError.data, diff --git a/apps/server/archive/contexts/accounts/application/update-account.use-case.ts b/apps/server/archive/contexts/accounts/application/update-account.use-case.ts index 6abd3696..067fe5e8 100644 --- a/apps/server/archive/contexts/accounts/application/update-account.use-case.ts +++ b/apps/server/archive/contexts/accounts/application/update-account.use-case.ts @@ -46,8 +46,8 @@ export class UpdateAccountUseCase { const errors: Error[] = []; const validatedData: Partial = {}; - if (dto.is_freelancer) { - validatedData.isFreelancer = dto.is_freelancer; + if (dto.is_companyr) { + validatedData.isFreelancer = dto.is_companyr; } if (dto.name) { diff --git a/apps/server/archive/contexts/accounts/domain/services/account-service.integration.test.ts b/apps/server/archive/contexts/accounts/domain/services/account-service.integration.test.ts index 4297405f..773949e6 100644 --- a/apps/server/archive/contexts/accounts/domain/services/account-service.integration.test.ts +++ b/apps/server/archive/contexts/accounts/domain/services/account-service.integration.test.ts @@ -22,7 +22,7 @@ const mockAccountRepository: IAccountRepository = { const sampleAccountPrimitives = { id: "c5743279-e1cf-4dd5-baae-6698c8c6183c", - is_freelancer: false, + is_companyr: false, name: "Empresa XYZ", trade_name: "XYZ Trading", tin: "123456789", @@ -72,7 +72,7 @@ const accountBuilder = (accountData: any) => { const validatedData: IAccountProps = { status: AccountStatus.createInactive(), - isFreelancer: sampleAccountPrimitives.is_freelancer, + isFreelancer: sampleAccountPrimitives.is_companyr, name: sampleAccountPrimitives.name, tradeName: sampleAccountPrimitives.trade_name ? Maybe.some(sampleAccountPrimitives.trade_name) diff --git a/apps/server/archive/contexts/accounts/domain/services/account-service.test.ts b/apps/server/archive/contexts/accounts/domain/services/account-service.test.ts index 8a362f39..c7d28b66 100644 --- a/apps/server/archive/contexts/accounts/domain/services/account-service.test.ts +++ b/apps/server/archive/contexts/accounts/domain/services/account-service.test.ts @@ -14,7 +14,7 @@ const mockAccountRepository: IAccountRepository = { const sampleAccount = { id: "c5743279-e1cf-4dd5-baae-6698c8c6183c", - is_freelancer: false, + is_companyr: false, name: "Empresa XYZ", trade_name: "XYZ Trading", tin: "123456789", diff --git a/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts b/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts index 150b68ec..2d7bcf28 100644 --- a/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts +++ b/apps/server/archive/contexts/accounts/infraestructure/mappers/account.mapper.ts @@ -53,7 +53,7 @@ export class AccountMapper return Account.create( { status: statusOrError.data, - isFreelancer: source.is_freelancer, + isFreelancer: source.is_companyr, name: source.name, tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(), tin: tinOrError.data, @@ -75,7 +75,7 @@ export class AccountMapper public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes { return { id: source.id.toPrimitive(), - is_freelancer: source.isFreelancer, + is_companyr: source.isFreelancer, name: source.name, trade_name: source.tradeName.getOrUndefined(), tin: source.tin.toPrimitive(), diff --git a/apps/server/archive/contexts/accounts/infraestructure/sequelize/account.model.ts b/apps/server/archive/contexts/accounts/infraestructure/sequelize/account.model.ts index e41381f5..a0ad589b 100644 --- a/apps/server/archive/contexts/accounts/infraestructure/sequelize/account.model.ts +++ b/apps/server/archive/contexts/accounts/infraestructure/sequelize/account.model.ts @@ -17,7 +17,7 @@ export class AccountModel extends Model, AccountCr declare id: string; - declare is_freelancer: boolean; + declare is_companyr: boolean; declare name: string; declare trade_name: CreationOptional; declare tin: string; @@ -49,7 +49,7 @@ export default (sequelize: Sequelize) => { type: DataTypes.UUID, primaryKey: true, }, - is_freelancer: { + is_companyr: { type: DataTypes.BOOLEAN, allowNull: false, }, diff --git a/apps/server/archive/contexts/accounts/presentation/controllers/create-account/create-account.presenter.ts b/apps/server/archive/contexts/accounts/presentation/controllers/create-account/create-account.presenter.ts index 028b9bad..3b30b366 100644 --- a/apps/server/archive/contexts/accounts/presentation/controllers/create-account/create-account.presenter.ts +++ b/apps/server/archive/contexts/accounts/presentation/controllers/create-account/create-account.presenter.ts @@ -10,7 +10,7 @@ export const createAccountPresenter: ICreateAccountPresenter = { toDTO: (account: Account): ICreateAccountResponseDTO => ({ id: ensureString(account.id.toString()), - is_freelancer: ensureBoolean(account.isFreelancer), + is_companyr: ensureBoolean(account.isFreelancer), name: ensureString(account.name), trade_name: ensureString(account.tradeName.getOrUndefined()), tin: ensureString(account.tin.toString()), diff --git a/apps/server/archive/contexts/accounts/presentation/controllers/get-account/get-account.presenter.ts b/apps/server/archive/contexts/accounts/presentation/controllers/get-account/get-account.presenter.ts index a767f0a8..56941529 100644 --- a/apps/server/archive/contexts/accounts/presentation/controllers/get-account/get-account.presenter.ts +++ b/apps/server/archive/contexts/accounts/presentation/controllers/get-account/get-account.presenter.ts @@ -10,7 +10,7 @@ export const getAccountPresenter: IGetAccountPresenter = { toDTO: (account: Account): IGetAccountResponseDTO => ({ id: ensureString(account.id.toPrimitive()), - is_freelancer: ensureBoolean(account.isFreelancer), + is_companyr: ensureBoolean(account.isFreelancer), name: ensureString(account.name), trade_name: ensureString(account.tradeName.getOrUndefined()), tin: ensureString(account.tin.toPrimitive()), diff --git a/apps/server/archive/contexts/accounts/presentation/controllers/list-accounts/list-accounts.presenter.ts b/apps/server/archive/contexts/accounts/presentation/controllers/list-accounts/list-accounts.presenter.ts index 630af6dc..b7f887be 100644 --- a/apps/server/archive/contexts/accounts/presentation/controllers/list-accounts/list-accounts.presenter.ts +++ b/apps/server/archive/contexts/accounts/presentation/controllers/list-accounts/list-accounts.presenter.ts @@ -11,7 +11,7 @@ export const listAccountsPresenter: IListAccountsPresenter = { accounts.map((account) => ({ id: ensureString(account.id.toString()), - is_freelancer: ensureBoolean(account.isFreelancer), + is_companyr: ensureBoolean(account.isFreelancer), name: ensureString(account.name), trade_name: ensureString(account.tradeName.getOrUndefined()), tin: ensureString(account.tin.toString()), diff --git a/apps/server/archive/contexts/accounts/presentation/controllers/update-account/update-account.presenter.ts b/apps/server/archive/contexts/accounts/presentation/controllers/update-account/update-account.presenter.ts index 7b5ff671..201d98d2 100644 --- a/apps/server/archive/contexts/accounts/presentation/controllers/update-account/update-account.presenter.ts +++ b/apps/server/archive/contexts/accounts/presentation/controllers/update-account/update-account.presenter.ts @@ -10,7 +10,7 @@ export const updateAccountPresenter: IUpdateAccountPresenter = { toDTO: (account: Account): IUpdateAccountResponseDTO => ({ id: ensureString(account.id.toString()), - is_freelancer: ensureBoolean(account.isFreelancer), + is_companyr: ensureBoolean(account.isFreelancer), name: ensureString(account.name), trade_name: ensureString(account.tradeName.getOrUndefined()), tin: ensureString(account.tin.toString()), diff --git a/apps/server/archive/contexts/accounts/presentation/dto/accounts.request.dto.ts b/apps/server/archive/contexts/accounts/presentation/dto/accounts.request.dto.ts index 6731d9d5..9047b996 100644 --- a/apps/server/archive/contexts/accounts/presentation/dto/accounts.request.dto.ts +++ b/apps/server/archive/contexts/accounts/presentation/dto/accounts.request.dto.ts @@ -1,8 +1,8 @@ -export type IListAccountsRequestDTO = {} +export type IListAccountsRequestDTO = {}; export interface ICreateAccountRequestDTO { id: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; @@ -27,7 +27,7 @@ export interface ICreateAccountRequestDTO { } export interface IUpdateAccountRequestDTO { - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; diff --git a/apps/server/archive/contexts/accounts/presentation/dto/accounts.response.dto.ts b/apps/server/archive/contexts/accounts/presentation/dto/accounts.response.dto.ts index 2ac225bd..129d90a9 100644 --- a/apps/server/archive/contexts/accounts/presentation/dto/accounts.response.dto.ts +++ b/apps/server/archive/contexts/accounts/presentation/dto/accounts.response.dto.ts @@ -1,7 +1,7 @@ export interface IListAccountsResponseDTO { id: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; @@ -29,7 +29,7 @@ export interface IListAccountsResponseDTO { export interface IGetAccountResponseDTO { id: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; @@ -57,7 +57,7 @@ export interface IGetAccountResponseDTO { export interface ICreateAccountResponseDTO { id: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; @@ -88,7 +88,7 @@ export interface ICreateAccountResponseDTO { export interface IUpdateAccountResponseDTO { id: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; diff --git a/apps/server/archive/contexts/accounts/presentation/dto/accounts.schemas.ts b/apps/server/archive/contexts/accounts/presentation/dto/accounts.schemas.ts index de6e5cda..e85ec8e2 100644 --- a/apps/server/archive/contexts/accounts/presentation/dto/accounts.schemas.ts +++ b/apps/server/archive/contexts/accounts/presentation/dto/accounts.schemas.ts @@ -7,7 +7,7 @@ export const IGetAccountRequestSchema = z.object({}); export const ICreateAccountRequestSchema = z.object({ id: z.string(), - is_freelancer: z.boolean(), + is_companyr: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), @@ -35,7 +35,7 @@ export const ICreateAccountRequestSchema = z.object({ export const IUpdateAccountRequestSchema = z.object({ id: z.string(), - is_freelancer: z.boolean(), + is_companyr: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), diff --git a/apps/server/archive/contexts/contacts/infraestructure/mappers/contact.mapper.ts b/apps/server/archive/contexts/contacts/infraestructure/mappers/contact.mapper.ts index 15f9af09..683f793e 100644 --- a/apps/server/archive/contexts/contacts/infraestructure/mappers/contact.mapper.ts +++ b/apps/server/archive/contexts/contacts/infraestructure/mappers/contact.mapper.ts @@ -50,7 +50,7 @@ export class ContactMapper return Contact.create( { - isFreelancer: source.is_freelancer, + isFreelancer: source.is_companyr, reference: source.reference, name: source.name, tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(), @@ -77,7 +77,7 @@ export class ContactMapper return Result.ok({ id: source.id.toString(), reference: source.reference, - is_freelancer: source.isFreelancer, + is_companyr: source.isFreelancer, name: source.name, trade_name: source.tradeName.getOrUndefined(), tin: source.tin.toString(), diff --git a/apps/server/archive/contexts/contacts/infraestructure/sequelize/contact.model.ts b/apps/server/archive/contexts/contacts/infraestructure/sequelize/contact.model.ts index 7dad8e28..b45d656a 100644 --- a/apps/server/archive/contexts/contacts/infraestructure/sequelize/contact.model.ts +++ b/apps/server/archive/contexts/contacts/infraestructure/sequelize/contact.model.ts @@ -21,7 +21,7 @@ export class ContactModel extends Model< declare id: string; declare reference: CreationOptional; - declare is_freelancer: boolean; + declare is_companyr: boolean; declare name: string; declare trade_name: CreationOptional; declare tin: string; @@ -56,7 +56,7 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: false, }, - is_freelancer: { + is_companyr: { type: DataTypes.BOOLEAN, allowNull: false, }, diff --git a/apps/server/archive/contexts/contacts/presentation/controllers/list/list-contacts.presenter.ts b/apps/server/archive/contexts/contacts/presentation/controllers/list/list-contacts.presenter.ts index 86bb3379..d5aeef70 100644 --- a/apps/server/archive/contexts/contacts/presentation/controllers/list/list-contacts.presenter.ts +++ b/apps/server/archive/contexts/contacts/presentation/controllers/list/list-contacts.presenter.ts @@ -12,7 +12,7 @@ export const listContactsPresenter: IListContactsPresenter = { id: ensureString(contact.id.toString()), reference: ensureString(contact.reference), - is_freelancer: ensureBoolean(contact.isFreelancer), + is_companyr: ensureBoolean(contact.isFreelancer), name: ensureString(contact.name), trade_name: ensureString(contact.tradeName.getOrUndefined()), tin: ensureString(contact.tin.toString()), diff --git a/apps/server/archive/contexts/contacts/presentation/dto/contacts.response.dto.ts b/apps/server/archive/contexts/contacts/presentation/dto/contacts.response.dto.ts index c28cdaa0..dc134533 100644 --- a/apps/server/archive/contexts/contacts/presentation/dto/contacts.response.dto.ts +++ b/apps/server/archive/contexts/contacts/presentation/dto/contacts.response.dto.ts @@ -2,7 +2,7 @@ export interface IListContactsResponseDTO { id: string; reference: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; diff --git a/apps/server/archive/contexts/customer-billing/infraestructure/sequelize/customer.model.ts b/apps/server/archive/contexts/customer-billing/infraestructure/sequelize/customer.model.ts index a3f673a6..b092ac60 100644 --- a/apps/server/archive/contexts/customer-billing/infraestructure/sequelize/customer.model.ts +++ b/apps/server/archive/contexts/customer-billing/infraestructure/sequelize/customer.model.ts @@ -21,7 +21,7 @@ export class CustomerModel extends Model< declare id: string; declare reference: CreationOptional; - declare is_freelancer: boolean; + declare is_companyr: boolean; declare name: string; declare trade_name: CreationOptional; declare tin: string; @@ -56,7 +56,7 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: false, }, - is_freelancer: { + is_companyr: { type: DataTypes.BOOLEAN, allowNull: false, }, diff --git a/apps/server/archive/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts b/apps/server/archive/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts index 0246ad3c..144974f4 100644 --- a/apps/server/archive/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts +++ b/apps/server/archive/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts @@ -13,7 +13,7 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = { id: ensureString(customer.id.toString()), /*reference: ensureString(customer.), - is_freelancer: ensureBoolean(customer.isFreelancer), + is_companyr: ensureBoolean(customer.isFreelancer), name: ensureString(customer.name), trade_name: ensureString(customer.tradeName.getValue()), tin: ensureString(customer.tin.toString()), diff --git a/apps/server/archive/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts b/apps/server/archive/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts index 784aa0e8..c4d3f9af 100644 --- a/apps/server/archive/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts +++ b/apps/server/archive/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts @@ -2,7 +2,7 @@ export interface IListCustomerInvoicesResponseDTO { id: string; /*reference: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; @@ -26,4 +26,4 @@ export interface IListCustomerInvoicesResponseDTO { currency_code: string;*/ } -export type IGetCustomerInvoiceResponseDTO = {} +export type IGetCustomerInvoiceResponseDTO = {}; diff --git a/apps/server/archive/contexts/customer-billing/presentation/dto/customers.response.dto.ts b/apps/server/archive/contexts/customer-billing/presentation/dto/customers.response.dto.ts index 4ba91903..7f78ab6b 100644 --- a/apps/server/archive/contexts/customer-billing/presentation/dto/customers.response.dto.ts +++ b/apps/server/archive/contexts/customer-billing/presentation/dto/customers.response.dto.ts @@ -2,7 +2,7 @@ export interface IListCustomersResponseDTO { id: string; reference: string; - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; diff --git a/apps/server/package.json b/apps/server/package.json index 59848912..5bf289a5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -79,7 +79,7 @@ "entry": ["src/index.ts"], "outDir": "dist", "format": ["esm", "cjs"], - "target": "es2020", + "target": "ES2022", "sourcemap": true, "clean": true, "dts": true, diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index d734c372..cf101870 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -33,7 +33,7 @@ export const App = () => { baseURL: import.meta.env.VITE_API_SERVER_URL, getAccessToken, onAuthError: () => { - console.error("Error de autenticación"); + console.error("APP, Error de autenticación"); clearAccessToken(); //window.location.href = "/login"; // o usar navegación programática }, diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index 31396066..10ad1b54 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -5,9 +5,9 @@ "@/*": ["./src/*"] }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/modules.bak/invoices/src/server/presentation/dto/invoices.request.dto.ts b/modules.bak/invoices/src/server/presentation/dto/invoices.request.dto.ts index 6964685d..776096a0 100644 --- a/modules.bak/invoices/src/server/presentation/dto/invoices.request.dto.ts +++ b/modules.bak/invoices/src/server/presentation/dto/invoices.request.dto.ts @@ -1,4 +1,4 @@ -export interface IListInvoicesRequestDTO {} +export type IListInvoicesRequestDTO = {}; export interface ICreateInvoiceRequestDTO { id: string; @@ -12,7 +12,7 @@ export interface ICreateInvoiceRequestDTO { } export interface IUpdateInvoiceRequestDTO { - is_freelancer: boolean; + is_companyr: boolean; name: string; trade_name: string; tin: string; diff --git a/modules/auth/tsconfig.json b/modules/auth/tsconfig.json index 5ae37f70..38385361 100644 --- a/modules/auth/tsconfig.json +++ b/modules/auth/tsconfig.json @@ -7,9 +7,9 @@ }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/modules/core/src/api/application/errors/application-error.ts b/modules/core/src/api/application/errors/application-error.ts new file mode 100644 index 00000000..ee3dd16a --- /dev/null +++ b/modules/core/src/api/application/errors/application-error.ts @@ -0,0 +1,12 @@ +/** + * 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 new file mode 100644 index 00000000..7636f588 --- /dev/null +++ b/modules/core/src/api/application/errors/index.ts @@ -0,0 +1 @@ +export * from "./application-error"; diff --git a/modules/core/src/api/application/index.ts b/modules/core/src/api/application/index.ts new file mode 100644 index 00000000..49bbc161 --- /dev/null +++ b/modules/core/src/api/application/index.ts @@ -0,0 +1 @@ +export * from "./errors"; diff --git a/modules/core/src/api/domain/errors/domain-error.ts b/modules/core/src/api/domain/errors/domain-error.ts new file mode 100644 index 00000000..1618e4f5 --- /dev/null +++ b/modules/core/src/api/domain/errors/domain-error.ts @@ -0,0 +1,12 @@ +/** + * Errores de capa de dominio. No deben "filtrarse" a cliente tal cual. + * + * */ + +export class DomainError extends Error { + public readonly layer = "domain" as const; + constructor(message: string, options?: ErrorOptions) { + super(message, options); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/modules/core/src/api/domain/errors/domain-validation-error.ts b/modules/core/src/api/domain/errors/domain-validation-error.ts new file mode 100644 index 00000000..c233b2ba --- /dev/null +++ b/modules/core/src/api/domain/errors/domain-validation-error.ts @@ -0,0 +1,52 @@ +/** + * Clase DomainValidationError + * Representa un error de validación de dominio. + * + * Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores + * específicos de validación dentro del dominio de la aplicación. Permite identificar + * el código de error, el campo afectado y un detalle descriptivo del error. + * + * @class DomainValidationError + * @extends {Error} + * @property {string} code - Código del error de validación. + * @property {string} field - Campo afectado por el error de validación. + * @property {string} detail - Detalle descriptivo del error de validación. + * + * @example + * const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido"); + * console.error(error); + */ + +import { DomainError } from "./domain-error"; + +export class DomainValidationError extends DomainError { + // Discriminante estable para mapeo/telemetría + public readonly kind = "VALIDATION" as const; + + constructor( + public readonly code: string, // id de regla del negocio (ej. 'INVALID_FORMAT') + public readonly field: string, // path: 'number' | 'date' | 'lines[0].quantity' + public readonly detail: string, // mensaje legible del negocio + options?: ErrorOptions + ) { + super(`[${field}] ${detail}`, options); + this.name = "DomainValidationError"; + Object.freeze(this); + } + + // Constructores rápidos + static required(field: string, options?: ErrorOptions) { + return new DomainValidationError("REQUIRED", field, "cannot be empty", options); + } + static invalidFormat(field: string, detail = "invalid format", options?: ErrorOptions) { + return new DomainValidationError("INVALID_FORMAT", field, detail, options); + } + + // Proyección útil para Problem+JSON o colecciones + toDetail() { + return { path: this.field, message: this.detail, rule: this.code }; + } +} + +export const isDomainValidationError = (e: unknown): e is DomainValidationError => + e instanceof DomainValidationError; diff --git a/modules/core/src/api/domain/errors/duplicate-entity-error.ts b/modules/core/src/api/domain/errors/duplicate-entity-error.ts new file mode 100644 index 00000000..d7072859 --- /dev/null +++ b/modules/core/src/api/domain/errors/duplicate-entity-error.ts @@ -0,0 +1,11 @@ +import { DomainError } from "./domain-error"; + +export class DuplicateEntityError extends DomainError { + constructor(entity: string, field: string, value: string, options?: ErrorOptions) { + super(`Entity '${entity}' with field '${field}' and value '${value}' already exists.`, options); + this.name = "DuplicateEntityError"; + } +} + +export const isDuplicateEntityError = (e: unknown): e is DuplicateEntityError => + e instanceof DuplicateEntityError; diff --git a/modules/core/src/api/domain/errors/entity-not-found-error.ts b/modules/core/src/api/domain/errors/entity-not-found-error.ts new file mode 100644 index 00000000..ed262f00 --- /dev/null +++ b/modules/core/src/api/domain/errors/entity-not-found-error.ts @@ -0,0 +1,11 @@ +import { DomainError } from "./domain-error"; + +export class EntityNotFoundError extends DomainError { + constructor(entity: string, field: string, value: string, options?: ErrorOptions) { + super(`Entity '${entity}' with ${field} '${value}' was not found.`, options); + this.name = "EntityNotFoundError"; + } +} + +export const isEntityNotFoundError = (e: unknown): e is EntityNotFoundError => + e instanceof EntityNotFoundError; diff --git a/modules/core/src/api/domain/errors/index.ts b/modules/core/src/api/domain/errors/index.ts new file mode 100644 index 00000000..349cb2d5 --- /dev/null +++ b/modules/core/src/api/domain/errors/index.ts @@ -0,0 +1,5 @@ +export * from "./domain-error"; +export * from "./domain-validation-error"; +export * from "./duplicate-entity-error"; +export * from "./entity-not-found-error"; +export * from "./validation-error-collection"; diff --git a/modules/core/src/api/errors/validation-error-collection.ts b/modules/core/src/api/domain/errors/validation-error-collection.ts similarity index 65% rename from modules/core/src/api/errors/validation-error-collection.ts rename to modules/core/src/api/domain/errors/validation-error-collection.ts index 890f8b5b..67f7fe31 100644 --- a/modules/core/src/api/errors/validation-error-collection.ts +++ b/modules/core/src/api/domain/errors/validation-error-collection.ts @@ -15,19 +15,30 @@ * */ +import { DomainError } from "./domain-error"; + export interface ValidationErrorDetail { path: string; // ejemplo: "lines[1].unitPrice.amount" - message: string; // ejemplo: "Amount must be a positive number" + message: string; // ejemplo: "Amount must be a positive number", } -export class ValidationErrorCollection extends Error { +/** + * Error de validación múltiple. Agrega varios fallos de una sola vez. + */ + +export class ValidationErrorCollection extends DomainError { + public readonly code = "VALIDATION" as const; public readonly details: ValidationErrorDetail[]; - constructor(details: ValidationErrorDetail[]) { - super("Validation failed"); + constructor(message: string, details: ValidationErrorDetail[], options?: ErrorOptions) { + super(message, options); Object.setPrototypeOf(this, ValidationErrorCollection.prototype); this.name = "ValidationErrorCollection"; this.details = details; + Object.freeze(this); } } + +export const isValidationErrorCollection = (e: unknown): e is ValidationErrorCollection => + e instanceof ValidationErrorCollection; diff --git a/modules/core/src/api/domain/index.ts b/modules/core/src/api/domain/index.ts new file mode 100644 index 00000000..49bbc161 --- /dev/null +++ b/modules/core/src/api/domain/index.ts @@ -0,0 +1 @@ +export * from "./errors"; diff --git a/modules/core/src/api/errors/domain-validation-error.ts b/modules/core/src/api/errors/domain-validation-error.ts deleted file mode 100644 index ed4fa698..00000000 --- a/modules/core/src/api/errors/domain-validation-error.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Clase DomainValidationError - * Representa un error de validación de dominio. - * - * Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores - * específicos de validación dentro del dominio de la aplicación. Permite identificar - * el código de error, el campo afectado y un detalle descriptivo del error. - * - * @class DomainValidationError - * @extends {Error} - * @property {string} code - Código del error de validación. - * @property {string} field - Campo afectado por el error de validación. - * @property {string} detail - Detalle descriptivo del error de validación. - * - * @example - * const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido"); - * console.error(error); - */ - -export class DomainValidationError extends Error { - constructor( - public readonly code: string, - public readonly field: string, - public readonly detail: string - ) { - super(`[${field}] ${detail}`); - this.name = "DomainValidationError"; - } -} diff --git a/modules/core/src/api/errors/duplicate-entity-error.ts b/modules/core/src/api/errors/duplicate-entity-error.ts deleted file mode 100644 index fa02c9aa..00000000 --- a/modules/core/src/api/errors/duplicate-entity-error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class DuplicateEntityError extends Error { - constructor(entity: string, id: string) { - super(`Entity '${entity}' with ID '${id}' already exists.`); - this.name = "DuplicateEntityError"; - } -} diff --git a/modules/core/src/api/errors/entity-not-found-error.ts b/modules/core/src/api/errors/entity-not-found-error.ts deleted file mode 100644 index 4bcd487a..00000000 --- a/modules/core/src/api/errors/entity-not-found-error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class EntityNotFoundError extends Error { - constructor(entity: string, id: string | number) { - super(`Entity '${entity}' with ID '${id}' was not found.`); - this.name = "EntityNotFoundError"; - } -} diff --git a/modules/core/src/api/errors/error-mapper.ts b/modules/core/src/api/errors/error-mapper.ts deleted file mode 100644 index 7be82c99..00000000 --- a/modules/core/src/api/errors/error-mapper.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - ConnectionError, - DatabaseError, - ForeignKeyConstraintError, - UniqueConstraintError, - ValidationError, -} from "sequelize"; - -import { ApiError } from "./api-error"; -import { ConflictApiError } from "./conflict-api-error"; -import { DomainValidationError } from "./domain-validation-error"; -import { DuplicateEntityError } from "./duplicate-entity-error"; -import { EntityNotFoundError } from "./entity-not-found-error"; -import { ForbiddenApiError } from "./forbidden-api-error"; -import { InternalApiError } from "./internal-api-error"; -import { NotFoundApiError } from "./not-found-api-error"; -import { UnauthorizedApiError } from "./unauthorized-api-error"; -import { UnavailableApiError } from "./unavailable-api-error"; -import { ValidationApiError } from "./validation-api-error"; -import { ValidationErrorCollection } from "./validation-error-collection"; - -export const errorMapper = { - toDomainError(error: unknown): Error { - if (error instanceof UniqueConstraintError) { - const field = error.errors[0]?.path || "unknown_field"; - return new Error(`A record with this ${field} already exists.`); - } - - if (error instanceof ForeignKeyConstraintError) { - return new Error("A referenced entity was not found or is invalid."); - } - - if (error instanceof ValidationError) { - return new Error(`Invalid data provided: ${error.message}`); - } - - if (error instanceof DatabaseError) { - return new Error("Database error occurred."); - } - - if (error instanceof Error) { - return error; // Fallback a error estándar - } - - return new Error("Unknown persistence error."); - }, - - /** - * Mapea errores de la aplicación a errores de la API. - * - * Esta función toma un error de la aplicación y lo convierte en un objeto ApiError - * adecuado para enviar como respuesta HTTP. Maneja errores comunes como validación, - * conflictos, no encontrados, autenticación y errores de infraestructura. - * - * @param error - El error de la aplicación a mapear. - * @returns Un objeto ApiError que representa el error mapeado. - * @example - * const error = new Error("Invalid input"); - * const apiError = errorMapper.toApiError(error); - * console.log(apiError); - * // Output: ValidationApiError { status: 422, title: 'Validation Failed', detail: 'Invalid input', type: 'https://httpstatuses.com/422' } - * @throws {ApiError} Si el error no puede ser mapeado a un tipo conocido. - * @see ApiError - * @see ValidationApiError - */ - toApiError: (error: Error): ApiError => { - const message = error.message || "An unexpected error occurred"; - - // 1. 🔍 Errores de validación complejos (agrupados) - if (error instanceof ValidationErrorCollection) { - return new ValidationApiError(error.message, error.details); - } - - // 2. 🔍 Errores individuales de validación de dominio - if (error instanceof DomainValidationError) { - return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]); - } - - if (error instanceof DuplicateEntityError) { - return new ConflictApiError(error.message); - } - - if (error instanceof EntityNotFoundError) { - return new NotFoundApiError(error.message); - } - - // 3. 🔍 Errores individuales de validación - if ( - message.includes("invalid") || - message.includes("is not valid") || - message.includes("must be") || - message.includes("cannot be") || - message.includes("empty") - ) { - return new ValidationApiError(message); - } - - // 4. 🔍 Recurso no encontrado - if (error.name === "NotFoundError" || message.includes("not found")) { - return new NotFoundApiError(message); - } - - // 5. 🔍 Conflicto (por ejemplo, duplicado) - if ( - error.name === "ConflictError" || - error instanceof UniqueConstraintError || - message.includes("already exists") || - message.includes("duplicate key") - ) { - return new ConflictApiError(message); - } - - // 6. 🔍 No autenticado - if (error.name === "UnauthorizedError" || message.includes("unauthorized")) { - return new UnauthorizedApiError(message); - } - - // 7. 🔍 Prohibido - if (error.name === "ForbiddenError" || message.includes("forbidden")) { - return new ForbiddenApiError(message); - } - - // 8. 🔍 Error de conexión o indisponibilidad de servicio - if ( - error instanceof ConnectionError || - message.includes("Database connection lost") || - message.includes("timeout") || - message.includes("ECONNREFUSED") - ) { - return new UnavailableApiError("Service temporarily unavailable."); - } - - // 9. 🔍 Fallback: error no identificado - return new InternalApiError(`Unexpected error: ${message}`); - }, -}; diff --git a/modules/core/src/api/index.ts b/modules/core/src/api/index.ts index d140fc71..e61c4ebe 100644 --- a/modules/core/src/api/index.ts +++ b/modules/core/src/api/index.ts @@ -1,4 +1,5 @@ -export * from "./errors"; +export * from "./application"; +export * from "./domain"; export * from "./infrastructure"; export * from "./logger"; export * from "./modules"; diff --git a/modules/core/src/api/infrastructure/errors/index.ts b/modules/core/src/api/infrastructure/errors/index.ts new file mode 100644 index 00000000..95be62e0 --- /dev/null +++ b/modules/core/src/api/infrastructure/errors/index.ts @@ -0,0 +1,3 @@ +export * from "./infrastructure-errors"; +export * from "./infrastructure-repository-error"; +export * from "./infrastructure-unavailable-error"; diff --git a/modules/core/src/api/infrastructure/errors/infrastructure-errors.ts b/modules/core/src/api/infrastructure/errors/infrastructure-errors.ts new file mode 100644 index 00000000..e3dee196 --- /dev/null +++ b/modules/core/src/api/infrastructure/errors/infrastructure-errors.ts @@ -0,0 +1,11 @@ +/** + * Errores de capa de infraestructura. No deben "filtrarse" a cliente tal cual. + * Se usan para decidir el código HTTP y para observabilidad (logs/tracing). */ + +export class InfrastructureError extends Error { + public readonly layer = "infrastructure" as const; + constructor(message: string, options?: ErrorOptions) { + super(message, options); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/modules/core/src/api/infrastructure/errors/infrastructure-repository-error.ts b/modules/core/src/api/infrastructure/errors/infrastructure-repository-error.ts new file mode 100644 index 00000000..abb018e4 --- /dev/null +++ b/modules/core/src/api/infrastructure/errors/infrastructure-repository-error.ts @@ -0,0 +1,11 @@ +import { InfrastructureError } from "./infrastructure-errors"; + +export class InfrastructureRepositoryError extends InfrastructureError { + public readonly code = "REPOSITORY_ERROR" as const; + constructor(message = "Repository operation failed", options?: ErrorOptions) { + super(message, options); + } +} + +export const isInfrastructureRepositoryError = (e: unknown): e is InfrastructureRepositoryError => + e instanceof InfrastructureRepositoryError; diff --git a/modules/core/src/api/infrastructure/errors/infrastructure-unavailable-error.ts b/modules/core/src/api/infrastructure/errors/infrastructure-unavailable-error.ts new file mode 100644 index 00000000..f6bfa7d6 --- /dev/null +++ b/modules/core/src/api/infrastructure/errors/infrastructure-unavailable-error.ts @@ -0,0 +1,11 @@ +import { InfrastructureError } from "./infrastructure-errors"; + +export class InfrastructureUnavailableError extends InfrastructureError { + public readonly code = "UNAVAILABLE" as const; + constructor(message = "Underlying service temporarily unavailable", options?: ErrorOptions) { + super(message, options); + } +} + +export const isInfrastructureUnavailableError = (e: unknown): e is InfrastructureUnavailableError => + e instanceof InfrastructureUnavailableError; diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts new file mode 100644 index 00000000..d61802b2 --- /dev/null +++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts @@ -0,0 +1,188 @@ +// Clase para mapear errores de Dominio/Aplicación/Infraestructura → ApiError (Problem+JSON). +// - Inmutable (register() devuelve una NUEVA instancia). +// - Extensible por reglas con prioridad. +// - Sin dependencias de vendors (p.ej. Sequelize). +// +// ─ Convenciones de carpetas sugeridas: +// domain/errors/* → errores semánticos (DDD) +// application/errors/* → errores de aplicación (servicios, lógica de negocio) +// infrastructure/errors/* → errores técnicos (DB, red, timeouts) +// infrastructure/express/errors/* → ApiError (RFC7807) y familia +// +// Nota: Todos los nombres de tipos/clases/archivos en inglés; comentarios en castellano. + +import { + DomainValidationError, + DuplicateEntityError, + EntityNotFoundError, + ValidationErrorCollection, + isDomainValidationError, + isDuplicateEntityError, + isEntityNotFoundError, + isValidationErrorCollection, +} from "../../domain"; +import { isInfrastructureRepositoryError, isInfrastructureUnavailableError } from "../errors"; +import { + ApiError, + ConflictApiError, + ForbiddenApiError, + InternalApiError, + NotFoundApiError, + UnauthorizedApiError, + UnavailableApiError, + ValidationApiError, +} from "./errors"; + +// ──────────────────────────────────────────────────────────────────────────────── +// Contexto opcional para enriquecer Problem+JSON (útil en middleware Express) +// ──────────────────────────────────────────────────────────────────────────────── +export interface ApiErrorContext { + instance?: string; // p.ej. req.originalUrl + correlationId?: string; // p.ej. header 'x-correlation-id' + method?: string; // GET/POST/PUT/DELETE +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Regla de mapeo: cómo reconocer y construir un ApiError +// ──────────────────────────────────────────────────────────────────────────────── +export interface ErrorToApiRule { + priority?: number; // mayor valor ⇒ se evalúa antes (default 0) + matches: (e: unknown) => boolean; + build: (e: unknown, ctx?: ApiErrorContext) => ApiError; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// ApiErrorMapper (inmutable) +// ──────────────────────────────────────────────────────────────────────────────── +export class ApiErrorMapper { + private readonly rules: ReadonlyArray; + private readonly fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError; + + private constructor( + rules: ReadonlyArray, + fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError + ) { + this.rules = [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); + this.fallback = fallback; + Object.freeze(this); + } + + // Crea un mapper con reglas por defecto (cubren casos comunes) + static default(): ApiErrorMapper { + return new ApiErrorMapper(defaultRules, defaultFallback); + } + + // Registra una regla adicional devolviendo una NUEVA instancia + register(rule: ErrorToApiRule): ApiErrorMapper { + return new ApiErrorMapper([...this.rules, rule], this.fallback); + } + + // Mapea un error a un ApiError evaluando reglas por prioridad + map(error: unknown, ctx?: ApiErrorContext): ApiError { + for (const rule of this.rules) { + try { + if (rule.matches(error)) { + return rule.build(error, ctx); + } + } catch { + // ⚠️ Una regla no debe tumbar el mapper; continuamos con la siguiente. + // continue + } + } + return this.fallback(error, ctx); + } + + // Útil en tests / introspección + getRules(): ReadonlyArray { + return this.rules; + } +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Reglas por defecto (prioridad alta a más específicas) +// ──────────────────────────────────────────────────────────────────────────────── +const defaultRules: ReadonlyArray = [ + // 1) Validación múltiple (colección) + { + priority: 100, + matches: (e) => isValidationErrorCollection(e), + build: (e) => + new ValidationApiError( + (e as ValidationErrorCollection).message, + (e as ValidationErrorCollection).details + ), + }, + + // 2) Validación de dominio unitaria + { + priority: 90, + matches: (e) => isDomainValidationError(e), + build: (e) => + new ValidationApiError((e as DomainValidationError).detail, [ + { path: (e as DomainValidationError).field, message: (e as DomainValidationError).detail }, + ]), + }, + + // 3) Duplicados / conflictos de unicidad + { + priority: 80, + matches: (e) => isDuplicateEntityError(e), + build: (e) => new ConflictApiError((e as DuplicateEntityError).message), + }, + + // 4) No encontrado + { + priority: 70, + matches: (e) => isEntityNotFoundError(e), + build: (e) => new NotFoundApiError((e as EntityNotFoundError).message), + }, + + // 5) Infra transitoria (DB/servicio caído, timeouts) + { + priority: 60, + matches: (e) => isInfrastructureUnavailableError(e), + build: () => new UnavailableApiError("Service temporarily unavailable."), + }, + + // 6) Infra no transitoria (errores de repositorio inesperados) + { + priority: 50, + matches: (e) => isInfrastructureRepositoryError(e), + build: () => new InternalApiError("Unexpected repository error."), + }, + + // 7) Autenticación/autorización por nombre (si no tienes clases dedicadas) + { + priority: 40, + matches: (e): e is Error => e instanceof Error && e.name === "UnauthorizedError", + build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"), + }, + { + priority: 40, + matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError", + build: (e) => new ForbiddenApiError((e as Error).message || "Forbidden"), + }, +]; + +// Fallback genérico (500) +function defaultFallback(e: unknown): ApiError { + const message = typeof (e as any)?.message === "string" ? (e as any).message : "Unexpected error"; + return new InternalApiError(`Unexpected error: ${message}`); +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Serializador opcional a Problem+JSON (si tu ApiError no lo trae ya) +// ──────────────────────────────────────────────────────────────────────────────── +export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) { + const maybeErrors = (apiError as any).errors ? { errors: (apiError as any).errors } : {}; + return { + type: apiError.type, + title: apiError.title, + status: apiError.status, + detail: apiError.detail, + ...(ctx?.instance ? { instance: ctx.instance } : {}), + ...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}), + ...(ctx?.method ? { method: ctx.method } : {}), + ...maybeErrors, + }; +} diff --git a/modules/core/src/api/errors/api-error.ts b/modules/core/src/api/infrastructure/express/errors/api-error.ts similarity index 89% rename from modules/core/src/api/errors/api-error.ts rename to modules/core/src/api/infrastructure/express/errors/api-error.ts index 6cac5a8d..c92d1b6f 100644 --- a/modules/core/src/api/errors/api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/api-error.ts @@ -1,3 +1,5 @@ +import { InfrastructureError } from "../../errors"; + interface IApiErrorOptions { status: number; title: string; @@ -8,7 +10,7 @@ interface IApiErrorOptions { [key: string]: any; // Para permitir añadir campos extra } -export class ApiError extends Error { +export class ApiError extends InfrastructureError { public status: number; public title: string; public detail: string; diff --git a/modules/core/src/api/errors/conflict-api-error.ts b/modules/core/src/api/infrastructure/express/errors/conflict-api-error.ts similarity index 99% rename from modules/core/src/api/errors/conflict-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/conflict-api-error.ts index bd903221..870d29f3 100644 --- a/modules/core/src/api/errors/conflict-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/conflict-api-error.ts @@ -9,4 +9,4 @@ export class ConflictApiError extends ApiError { type: "https://httpstatuses.com/409", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/forbidden-api-error.ts b/modules/core/src/api/infrastructure/express/errors/forbidden-api-error.ts similarity index 99% rename from modules/core/src/api/errors/forbidden-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/forbidden-api-error.ts index c833751f..852a724d 100644 --- a/modules/core/src/api/errors/forbidden-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/forbidden-api-error.ts @@ -9,4 +9,4 @@ export class ForbiddenApiError extends ApiError { type: "https://httpstatuses.com/403", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/index.ts b/modules/core/src/api/infrastructure/express/errors/index.ts similarity index 59% rename from modules/core/src/api/errors/index.ts rename to modules/core/src/api/infrastructure/express/errors/index.ts index 1e3c8a02..25c913e2 100644 --- a/modules/core/src/api/errors/index.ts +++ b/modules/core/src/api/infrastructure/express/errors/index.ts @@ -1,13 +1,8 @@ export * from "./api-error"; export * from "./conflict-api-error"; -export * from "./domain-validation-error"; -export * from "./duplicate-entity-error"; -export * from "./entity-not-found-error"; -export * from "./error-mapper"; export * from "./forbidden-api-error"; export * from "./internal-api-error"; export * from "./not-found-api-error"; export * from "./unauthorized-api-error"; export * from "./unavailable-api-error"; export * from "./validation-api-error"; -export * from "./validation-error-collection"; diff --git a/modules/core/src/api/errors/internal-api-error.ts b/modules/core/src/api/infrastructure/express/errors/internal-api-error.ts similarity index 99% rename from modules/core/src/api/errors/internal-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/internal-api-error.ts index 2c511d0c..8570f40a 100644 --- a/modules/core/src/api/errors/internal-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/internal-api-error.ts @@ -9,4 +9,4 @@ export class InternalApiError extends ApiError { type: "https://httpstatuses.com/500", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/not-found-api-error.ts b/modules/core/src/api/infrastructure/express/errors/not-found-api-error.ts similarity index 99% rename from modules/core/src/api/errors/not-found-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/not-found-api-error.ts index f2cd605d..532add2d 100644 --- a/modules/core/src/api/errors/not-found-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/not-found-api-error.ts @@ -9,4 +9,4 @@ export class NotFoundApiError extends ApiError { type: "https://httpstatuses.com/404", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/unauthorized-api-error.ts b/modules/core/src/api/infrastructure/express/errors/unauthorized-api-error.ts similarity index 99% rename from modules/core/src/api/errors/unauthorized-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/unauthorized-api-error.ts index d36453cd..6d3ab2e8 100644 --- a/modules/core/src/api/errors/unauthorized-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/unauthorized-api-error.ts @@ -9,4 +9,4 @@ export class UnauthorizedApiError extends ApiError { type: "https://httpstatuses.com/401", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/unavailable-api-error.ts b/modules/core/src/api/infrastructure/express/errors/unavailable-api-error.ts similarity index 99% rename from modules/core/src/api/errors/unavailable-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/unavailable-api-error.ts index 8edf9edd..6aac3098 100644 --- a/modules/core/src/api/errors/unavailable-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/unavailable-api-error.ts @@ -9,4 +9,4 @@ export class UnavailableApiError extends ApiError { type: "https://httpstatuses.com/503", }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/errors/validation-api-error.ts b/modules/core/src/api/infrastructure/express/errors/validation-api-error.ts similarity index 99% rename from modules/core/src/api/errors/validation-api-error.ts rename to modules/core/src/api/infrastructure/express/errors/validation-api-error.ts index 9498e62b..3f593034 100644 --- a/modules/core/src/api/errors/validation-api-error.ts +++ b/modules/core/src/api/infrastructure/express/errors/validation-api-error.ts @@ -10,4 +10,4 @@ export class ValidationApiError extends ApiError { errors, }); } -} \ No newline at end of file +} diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index cb318d22..c99d57d2 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -10,7 +10,7 @@ import { UnauthorizedApiError, UnavailableApiError, ValidationApiError, -} from "../../errors"; +} from "./errors"; type GuardResultLike = { isFailure: boolean; error?: ApiError }; export type GuardContext = { diff --git a/modules/core/src/api/infrastructure/express/express-guards.ts b/modules/core/src/api/infrastructure/express/express-guards.ts index 21d48da0..e80adcc7 100644 --- a/modules/core/src/api/infrastructure/express/express-guards.ts +++ b/modules/core/src/api/infrastructure/express/express-guards.ts @@ -1,4 +1,4 @@ -import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "../../errors"; +import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors"; import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller"; // ─────────────────────────────────────────────────────────────────────────── diff --git a/modules/core/src/api/infrastructure/express/index.ts b/modules/core/src/api/infrastructure/express/index.ts index 49108bdf..a3c86a72 100644 --- a/modules/core/src/api/infrastructure/express/index.ts +++ b/modules/core/src/api/infrastructure/express/index.ts @@ -1,3 +1,5 @@ +export * from "./api-error-mapper"; +export * from "./errors"; export * from "./express-controller"; export * from "./express-guards"; export * from "./middlewares"; diff --git a/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts b/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts index 59742485..8386f6be 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts @@ -1,5 +1,6 @@ import { NextFunction, Request, Response } from "express"; -import { ApiError } from "../../../errors/api-error"; +import { ApiErrorMapper, toProblemJson } from "../api-error-mapper"; +import { ApiError } from "../errors/api-error"; export const globalErrorHandler = async ( error: Error, @@ -11,6 +12,19 @@ export const globalErrorHandler = async ( if (res.headersSent) { return next(error); } + const ctx = { + instance: req.originalUrl, + correlationId: (req.headers["x-correlation-id"] as string) || undefined, + method: req.method, + }; + + const apiError = ApiErrorMapper.map(err, ctx); + const body = toProblemJson(apiError, ctx); + + // 👇 Log interno con cause/traza (no lo exponemos al cliente) + // logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`); + + res.status(apiError.status).json(body); //logger.error(`❌ Unhandled API error: ${error.message}`); diff --git a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts b/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts index 5a1cd881..7e4cfd63 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts @@ -1,6 +1,6 @@ import { RequestHandler } from "express"; import { ZodSchema } from "zod/v4"; -import { InternalApiError, ValidationApiError } from "../../../errors"; +import { InternalApiError, ValidationApiError } from "../errors"; import { ExpressController } from "../express-controller"; /** diff --git a/modules/core/src/api/infrastructure/index.ts b/modules/core/src/api/infrastructure/index.ts index 9aabe26e..e77d9642 100644 --- a/modules/core/src/api/infrastructure/index.ts +++ b/modules/core/src/api/infrastructure/index.ts @@ -1,3 +1,4 @@ export * from "./database"; +export * from "./errors"; export * from "./express"; export * from "./sequelize"; diff --git a/modules/core/src/api/infrastructure/sequelize/index.ts b/modules/core/src/api/infrastructure/sequelize/index.ts index df6c2cb0..2a0722aa 100644 --- a/modules/core/src/api/infrastructure/sequelize/index.ts +++ b/modules/core/src/api/infrastructure/sequelize/index.ts @@ -1,3 +1,4 @@ +export * from "./sequelize-error-translator"; export * from "./sequelize-mapper"; export * from "./sequelize-repository"; export * from "./sequelize-transaction-manager"; diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts new file mode 100644 index 00000000..c5385bea --- /dev/null +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-error-translator.ts @@ -0,0 +1,77 @@ +import { + ConnectionError, + DatabaseError, + ForeignKeyConstraintError, + ValidationError as SequelizeValidationError, + UniqueConstraintError, +} from "sequelize"; +import { + DomainValidationError, + DuplicateEntityError, + EntityNotFoundError, + ValidationErrorCollection, +} from "../../domain"; +import { InfrastructureRepositoryError } from "../errors/infrastructure-repository-error"; +import { InfrastructureUnavailableError } from "../errors/infrastructure-unavailable-error"; + +/** + * Traduce errores específicos de Sequelize a errores de dominio/infraestructura + * entendibles por el resto de capas. + * + * 👉 Este traductor pertenece a la infraestructura (persistencia) + */ +export function translateSequelizeError(err: unknown): Error { + // 1) Duplicados (índices únicos) + if (err instanceof UniqueConstraintError) { + // Tomamos el primer detalle (puede haber varios) + const detail = err.errors?.[0]; + const entity = detail?.instance?.constructor.name ?? "unknown_entity"; + const value = detail?.value ?? "unknown_value"; + const field = detail?.path ?? "unknown_field"; + + // ⚠️ Si los nombres de campo son sensibles, normaliza/whitelistea antes de exponerlos a dominio + return new DuplicateEntityError(entity, field, value, { cause: err }); + } + + // 2) Violación de FK → error de validación de dominio (referencia inválida) + if (err instanceof ForeignKeyConstraintError) { + // Sequelize expone `fields` (obj) y `index`. Extraemos el campo si está. + const entity = err.index ?? "unknown_entity"; + const field = err.fields ? Object.keys(err.fields)[0] : "foreign_key"; + const value = err.fields ? Object.values(err.fields)[0] : "unknown_value"; + return new EntityNotFoundError(entity, field, value, { cause: err }); + } + + // 3) Validaciones de Sequelize (pueden venir varias) + if (err instanceof SequelizeValidationError) { + const details = (err.errors ?? []).map((e) => ({ + path: e.path ?? "unknown", + message: e.message, + // Algunas props útiles: e.validatorKey / e.validatorName + rule: (e as any).validatorKey ?? undefined, + })); + + // Si sólo hay 1, puedes preferir DomainValidationError + if (details.length === 1) { + const d = details[0]; + return DomainValidationError.invalidFormat(d.path, d.message, { cause: err }); + } + + return new ValidationErrorCollection("Invalid data provided", details, { cause: err }); + } + + // 4) Conectividad / indisponibilidad (transitorio) + if (err instanceof ConnectionError) { + return new InfrastructureUnavailableError("Database connection unavailable", { cause: err }); + } + + // 5) Otros errores de base de datos (no transitorios) + if (err instanceof DatabaseError) { + return new InfrastructureRepositoryError("Database error occurred", { cause: err }); + } + + // 6) Fallback: deja pasar si ya es un Error tipado de tu app, si no wrap + if (err instanceof Error) return err; + + return new InfrastructureRepositoryError("Unknown persistence error", { cause: err as any }); +} diff --git a/modules/core/tsconfig.json b/modules/core/tsconfig.json index ff2b98c1..00dfcb5e 100644 --- a/modules/core/tsconfig.json +++ b/modules/core/tsconfig.json @@ -7,9 +7,9 @@ }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts index 2d2d38ca..48642ba1 100644 --- a/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts @@ -33,7 +33,7 @@ export class DeleteCustomerInvoiceUseCase { } if (!existsCheck.data) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString())); + return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); } return await this.service.deleteById(invoiceId, transaction); diff --git a/modules/customer-invoices/src/api/domain/errors/customer-invoice-id-already-exits-error.ts b/modules/customer-invoices/src/api/domain/errors/customer-invoice-id-already-exits-error.ts new file mode 100644 index 00000000..fc9f9e03 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/errors/customer-invoice-id-already-exits-error.ts @@ -0,0 +1,9 @@ +// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError +// (si defines un error más ubicuo dentro del BC con su propia clase) + +import { DomainError } from "@erp/core/api"; + +// Suponemos que existe esta clase en tu dominio de Billing: +export class CustomerInvoiceIdAlreadyExistsError extends DomainError { + public readonly code = "DUPLICATE_INVOICE_ID" as const; +} diff --git a/modules/customer-invoices/src/api/domain/errors/index.ts b/modules/customer-invoices/src/api/domain/errors/index.ts new file mode 100644 index 00000000..b8c78e2f --- /dev/null +++ b/modules/customer-invoices/src/api/domain/errors/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice-id-already-exits-error"; diff --git a/modules/customer-invoices/src/api/domain/index.ts b/modules/customer-invoices/src/api/domain/index.ts index 2c5c423d..ed8d70d5 100644 --- a/modules/customer-invoices/src/api/domain/index.ts +++ b/modules/customer-invoices/src/api/domain/index.ts @@ -1,5 +1,6 @@ export * from "./aggregates"; export * from "./entities"; +export * from "./errors"; export * from "./repositories"; export * from "./services"; export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts new file mode 100644 index 00000000..d633cda3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts @@ -0,0 +1,21 @@ +// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError +// (si defines un error más ubicuo dentro del BC con su propia clase) + +import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api"; +import { CustomerInvoiceIdAlreadyExistsError } from "../../domain"; + +// Crea una regla específica (prioridad alta para sobreescribir mensajes) +const invoiceDuplicateRule: ErrorToApiRule = { + priority: 120, + matches: (e): e is CustomerInvoiceIdAlreadyExistsError => + e instanceof CustomerInvoiceIdAlreadyExistsError, + build: (e) => + new ConflictApiError( + (e as CustomerInvoiceIdAlreadyExistsError).message || + "Invoice with the provided id already exists." + ), +}; + +// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra +export const customerInvoicesApiErrorMapper: ApiErrorMapper = + ApiErrorMapper.default().register(invoiceDuplicateRule); diff --git a/modules/customer-invoices/tsconfig.json b/modules/customer-invoices/tsconfig.json index 4c9e0112..b4a95fde 100644 --- a/modules/customer-invoices/tsconfig.json +++ b/modules/customer-invoices/tsconfig.json @@ -7,9 +7,9 @@ }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts index 4ed547b6..65da7e1d 100644 --- a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts +++ b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts @@ -6,6 +6,11 @@ import { ICustomerService } from "../../domain"; import { mapDTOToCustomerProps } from "../helpers"; import { CreateCustomersAssembler } from "./assembler"; +type CreateCustomerUseCaseInput = { + tenantId: string; + dto: CreateCustomerCommandDTO; +}; + export class CreateCustomerUseCase { constructor( private readonly service: ICustomerService, @@ -13,7 +18,9 @@ export class CreateCustomerUseCase { private readonly assembler: CreateCustomersAssembler ) {} - public execute(dto: CreateCustomerCommandDTO) { + public execute(params: CreateCustomerUseCaseInput) { + const { dto, tenantId: companyId } = params; + const customerPropsOrError = mapDTOToCustomerProps(dto); if (customerPropsOrError.isFailure) { @@ -42,7 +49,7 @@ export class CreateCustomerUseCase { return Result.fail(new DuplicateEntityError("Customer", id.toString())); } - const result = await this.service.save(newCustomer, transaction); + const result = await this.service.save(newCustomer, idCompany, transaction); if (result.isFailure) { return Result.fail(result.error); } diff --git a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts index d566f1d3..01e130eb 100644 --- a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts +++ b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts @@ -1,5 +1,4 @@ import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; -import { DeleteCustomerByIdQueryDTO } from "@erp/customers/common/dto"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { ICustomerService } from "../../domain"; @@ -21,17 +20,17 @@ export class DeleteCustomerUseCase { return this.transactionManager.complete(async (transaction) => { try { - const existsCheck = await this.service.existsById(id, transaction); + const existsCheck = await this.service.existsByIdInCompany(id, transaction); if (existsCheck.isFailure) { return Result.fail(existsCheck.error); } if (!existsCheck.data) { - return Result.fail(new EntityNotFoundError("Customer", id.toString())); + return Result.fail(new EntityNotFoundError("Customer", "id", id.toString())); } - return await this.service.deleteById(id, transaction); + return await this.service.deleteCustomerByIdInCompany(id, transaction); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts b/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts index 5e081600..bf0ed945 100644 --- a/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts +++ b/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts @@ -7,7 +7,7 @@ export class GetCustomerAssembler { id: customer.id.toPrimitive(), reference: customer.reference, - is_freelancer: customer.isFreelancer, + is_companyr: customer.isFreelancer, name: customer.name, trade_name: customer.tradeName.getOrUndefined(), tin: customer.tin.toPrimitive(), diff --git a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts index ab63721d..64c28b4b 100644 --- a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts +++ b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts @@ -1,10 +1,14 @@ import { ITransactionManager } from "@erp/core/api"; -import { GetCustomerByIdQueryDTO } from "@erp/customers/common/dto"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { ICustomerService } from "../../domain"; import { GetCustomerAssembler } from "./assembler"; +type GetCustomerUseCaseInput = { + tenantId: string; + id: string; +}; + export class GetCustomerUseCase { constructor( private readonly service: ICustomerService, @@ -12,16 +16,28 @@ export class GetCustomerUseCase { private readonly assembler: GetCustomerAssembler ) {} - public execute(dto: GetCustomerByIdQueryDTO) { - const idOrError = UniqueID.create(dto.id); + public execute(params: GetCustomerUseCaseInput) { + const { id, tenantId: companyId } = params; + + const idOrError = UniqueID.create(id); if (idOrError.isFailure) { return Result.fail(idOrError.error); } + const companyIdOrError = UniqueID.create(companyId); + + if (companyIdOrError.isFailure) { + return Result.fail(companyIdOrError.error); + } + return this.transactionManager.complete(async (transaction) => { try { - const customerOrError = await this.service.getById(idOrError.data, transaction); + const customerOrError = await this.service.getCustomerByIdInCompany( + companyIdOrError.data, + idOrError.data, + transaction + ); if (customerOrError.isFailure) { return Result.fail(customerOrError.error); } diff --git a/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts b/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts index 6bd623d1..62ef2f33 100644 --- a/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts +++ b/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts @@ -12,7 +12,7 @@ export class ListCustomersAssembler { id: customer.id.toPrimitive(), reference: customer.reference, - is_freelancer: customer.isFreelancer, + is_companyr: customer.isFreelancer, name: customer.name, trade_name: customer.tradeName.getOrUndefined() || "", tin: customer.tin.toString(), diff --git a/modules/customers/src/api/domain/aggregates/customer.ts b/modules/customers/src/api/domain/aggregates/customer.ts index 5a749375..e522f345 100644 --- a/modules/customers/src/api/domain/aggregates/customer.ts +++ b/modules/customers/src/api/domain/aggregates/customer.ts @@ -9,8 +9,9 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; export interface CustomerProps { + companyId: UniqueID; reference: string; - isFreelancer: boolean; + isCompany: boolean; name: string; tin: TINNumber; address: PostalAddress; @@ -29,6 +30,7 @@ export interface CustomerProps { export interface ICustomer { id: UniqueID; + companyId: UniqueID; reference: string; name: string; tin: TINNumber; @@ -44,8 +46,8 @@ export interface ICustomer { fax: Maybe; website: Maybe; - isCustomer: boolean; - isFreelancer: boolean; + isIndividual: boolean; + isCompany: boolean; isActive: boolean; } @@ -64,6 +66,15 @@ export class Customer extends AggregateRoot implements ICustomer return Result.ok(contact); } + update(partial: Partial>): Result { + const updatedCustomer = new Customer({ ...this.props, ...partial }, this.id); + return Result.ok(updatedCustomer); + } + + get companyId(): UniqueID { + return this.props.companyId; + } + get reference() { return this.props.reference; } @@ -116,12 +127,12 @@ export class Customer extends AggregateRoot implements ICustomer return this.props.currencyCode; } - get isCustomer(): boolean { - return !this.props.isFreelancer; + get isIndividual(): boolean { + return !this.props.isCompany; } - get isFreelancer(): boolean { - return this.props.isFreelancer; + get isCompany(): boolean { + return this.props.isCompany; } get isActive(): boolean { diff --git a/modules/customers/src/api/domain/repositories/customer-repository.interface.ts b/modules/customers/src/api/domain/repositories/customer-repository.interface.ts index c281b27b..3b6e5a9a 100644 --- a/modules/customers/src/api/domain/repositories/customer-repository.interface.ts +++ b/modules/customers/src/api/domain/repositories/customer-repository.interface.ts @@ -3,48 +3,50 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import { Customer } from "../aggregates"; +/** + * Contrato del repositorio de Customers. + * Define la interfaz de persistencia para el agregado `Customer`. + * El escopado multitenant está representado por `companyId`. + */ export interface ICustomerRepository { - existsById(id: UniqueID, transaction?: any): Promise>; + /** + * Guarda (crea o actualiza) un Customer en la base de datos. + * Retorna el objeto actualizado tras la operación. + */ + save(customer: Customer, transaction?: any): Promise>; /** - * - * Persiste una nueva factura o actualiza una existente. - * - * @param customer - El agregado a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result + * Comprueba si existe un Customer con un `id` dentro de una `company`. */ - save(customer: Customer, transaction: any): Promise>; + existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise>; /** - * - * Busca una factura por su identificador único. - * @param id - UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @returns Result + * Recupera un Customer por su ID y companyId. + * Devuelve un `NotFoundError` si no se encuentra. */ - findById(id: UniqueID, transaction: any): Promise>; + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise>; /** - * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result - * - * @see Criteria + * Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.). + * El resultado está encapsulado en un objeto `Collection`. */ - findByCriteria( + findByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, - transaction: any - ): Promise, Error>>; + transaction?: any + ): Promise>>; /** - * - * Elimina o marca como eliminada una factura. - * @param id - UUID de la factura a eliminar. - * @param transaction - Transacción activa para la operación. - * @returns Result + * Elimina un Customer por su ID, dentro de una empresa. + * Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía. */ - deleteById(id: UniqueID, transaction: any): Promise>; + deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: any): Promise>; } diff --git a/modules/customers/src/api/domain/services/customer-service.interface.ts b/modules/customers/src/api/domain/services/customer-service.interface.ts index be4afbff..073f4f78 100644 --- a/modules/customers/src/api/domain/services/customer-service.interface.ts +++ b/modules/customers/src/api/domain/services/customer-service.interface.ts @@ -4,30 +4,63 @@ import { Collection, Result } from "@repo/rdx-utils"; import { Customer, CustomerProps } from "../aggregates"; export interface ICustomerService { - build(props: CustomerProps, id?: UniqueID): Result; + /** + * Construye un nuevo Customer validando todos sus value objects. + */ + buildCustomerInCompany( + companyId: UniqueID, + props: Omit, + customerId?: UniqueID + ): Result; - save(invoice: Customer, transaction: any): Promise>; + /** + * Guarda un Customer (nuevo o modificado) en base de datos. + */ + saveCustomerInCompany(customer: Customer, transaction: any): Promise>; - existsById(id: UniqueID, transaction?: any): Promise>; + /** + * Comprueba si existe un Customer con ese ID en la empresa indicada. + */ + existsByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + transaction?: any + ): Promise>; - findByCriteria( + /** + * Lista todos los customers que cumplan el criterio, dentro de una empresa. + */ + findCustomerByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, transaction?: any ): Promise, Error>>; - getById(id: UniqueID, transaction?: any): Promise>; + /** + * Recupera un Customer por su ID dentro de una empresa. + */ + getCustomerByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + transaction?: any + ): Promise>; - updateById( - id: UniqueID, - data: Partial, + /** + * Actualiza parcialmente los datos de un Customer. + */ + updateCustomerByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + partial: Partial>, transaction?: any ): Promise>; - createCustomer( - id: UniqueID, - data: CustomerProps, + /** + * Elimina un Customer por ID dentro de una empresa. + */ + deleteCustomerByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, transaction?: any - ): Promise>; - - deleteById(id: UniqueID, transaction?: any): Promise>; + ): Promise>; } diff --git a/modules/customers/src/api/domain/services/customer.service.ts b/modules/customers/src/api/domain/services/customer.service.ts index 6fd51136..99f56723 100644 --- a/modules/customers/src/api/domain/services/customer.service.ts +++ b/modules/customers/src/api/domain/services/customer.service.ts @@ -1,23 +1,34 @@ 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 { Customer, CustomerProps } from "../aggregates"; import { ICustomerRepository } from "../repositories"; import { ICustomerService } from "./customer-service.interface"; export class CustomerService implements ICustomerService { constructor(private readonly repository: ICustomerRepository) {} + findCustomerByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: any + ): Promise, Error>> { + throw new Error("Method not implemented."); + } /** * Construye un nuevo agregado Customer a partir de props validadas. * - * @param props - Las propiedades ya validadas para crear la factura. - * @param id - Identificador UUID de la factura (opcional). + * @param companyId - Identificador de la empresa a la que pertenece el cliente. + * @param props - Las propiedades ya validadas para crear el cliente. + * @param customerId - Identificador UUID del cliente (opcional). * @returns Result - El agregado construido o un error si falla la creación. */ - build(props: CustomerProps, id?: UniqueID): Result { - return Customer.create(props, id); + buildCustomerInCompany( + companyId: UniqueID, + props: Omit, + customerId?: UniqueID + ): Result { + return Customer.create({ ...props, companyId }, customerId); } /** @@ -27,123 +38,107 @@ export class CustomerService implements ICustomerService { * @param transaction - Transacción activa para la operación. * @returns Result - El agregado guardado o un error si falla la operación. */ - async save(invoice: Customer, transaction: any): Promise> { - const saved = await this.repository.save(invoice, transaction); - return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error); + async saveCustomerInCompany( + customer: Customer, + transaction: any + ): Promise> { + return this.repository.save(customer, transaction); } /** * - * Comprueba si existe o no en persistencia una factura con el ID proporcionado + * Comprueba si existe o no en persistencia un cliente con el ID proporcionado * - * @param id - Identificador UUID de la factura. + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param customerId - Identificador UUID del cliente * @param transaction - Transacción activa para la operación. - * @returns Result - Existe la factura o no. + * @returns Result - Existe el cliente o no. */ - async existsById(id: UniqueID, transaction?: any): Promise> { - return this.repository.existsById(id, transaction); + existsByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + transaction?: any + ): Promise> { + return this.repository.existsByIdInCompany(companyId, customerId, transaction); } /** - * Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria. + * Obtiene una colección de clientes 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. + * @returns Result, Error> - Colección de clientes o error. */ - async findByCriteria( + async findCustomersByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, - transaction?: Transaction - ): Promise, Error>> { - const customersOrError = await this.repository.findByCriteria(criteria, transaction); - if (customersOrError.isFailure) { - console.error(customersOrError.error); - return Result.fail(customersOrError.error); - } - - // Solo devolver usuarios activos - //const allCustomers = customersOrError.data.filter((customer) => customer.isActive); - //return Result.ok(new Collection(allCustomers)); - - return customersOrError; + transaction?: any + ): Promise>> { + return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); } /** - * Recupera una factura por su identificador único. + * Recupera un cliente por su identificador único. * - * @param id - Identificador UUID de la factura. + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param customerId - Identificador UUID del cliente. * @param transaction - Transacción activa para la operación. - * @returns Result - Factura encontrada o error. + * @returns Result - Cliente encontradoF o error. */ - async getById(id: UniqueID, transaction?: Transaction): Promise> { - return await this.repository.findById(id, transaction); + async getCustomerByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + transaction?: any + ): Promise> { + return this.repository.getByIdInCompany(companyId, customerId, transaction); } /** - * Actualiza parcialmente una factura existente con nuevos datos. + * Actualiza parcialmente un cliente existente con nuevos datos. * - * @param id - Identificador de la factura a actualizar. - * @param changes - Subconjunto de props válidas para aplicar. + * @param companyId - Identificador de la empresa a la que pertenece el cliente. + * @param customerId - Identificador del cliente a actualizar. + * @param partial - Subconjunto de props válidas para aplicar. * @param transaction - Transacción activa para la operación. - * @returns Result - Factura actualizada o error. + * @returns Result - Cliente actualizado o error. */ - async updateById( + async updateCustomerByIdInCompany( + companyId: UniqueID, customerId: UniqueID, - changes: Partial, - transaction?: Transaction - ): Promise> { - // Verificar si la factura existe - const customerOrError = await this.repository.findById(customerId, transaction); - if (customerOrError.isFailure) { - return Result.fail(new Error("Customer not found")); + partial: Partial>, + transaction?: any + ): Promise> { + const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction); + + if (customerResult.isFailure) { + return Result.fail(customerResult.error); } - return Result.fail(new Error("No implementado")); + const customer = customerResult.data; + const updatedCustomer = customer.update(partial); - /*const updatedCustomerOrError = Customer.update(customerOrError.data, data); - if (updatedCustomerOrError.isFailure) { - return Result.fail( - new Error(`Error updating customer: ${updatedCustomerOrError.error.message}`) - ); + if (updatedCustomer.isFailure) { + return Result.fail(updatedCustomer.error); } - const updateCustomer = updatedCustomerOrError.data; - - await this.repo.update(updateCustomer, transaction); - return Result.ok(updateCustomer);*/ - } - - async createCustomer( - customerId: UniqueID, - data: CustomerProps, - transaction?: Transaction - ): Promise> { - // Verificar si la factura existe - const customerOrError = await this.repository.findById(customerId, transaction); - if (customerOrError.isSuccess) { - return Result.fail(new Error("Customer exists")); - } - - const newCustomerOrError = Customer.create(data, customerId); - if (newCustomerOrError.isFailure) { - return Result.fail(new Error(`Error creating customer: ${newCustomerOrError.error.message}`)); - } - - const newCustomer = newCustomerOrError.data; - - await this.repository.create(newCustomer, transaction); - return Result.ok(newCustomer); + return this.saveCustomerInCompany(updatedCustomer.data, transaction); } /** - * Elimina (o marca como eliminada) una factura según su ID. + * Elimina (o marca como eliminado) un cliente según su ID. * - * @param id - Identificador UUID de la factura. + * @param companyId - Identificador de la empresa a la que pertenece el cliente. + * @param customerId - Identificador UUID del cliente. * @param transaction - Transacción activa para la operación. * @returns Result - Resultado de la operación. */ - async deleteById(id: UniqueID, transaction?: Transaction): Promise> { - return this.repository.deleteById(id, transaction); + async deleteCustomerByIdInCompany( + companyId: UniqueID, + customerId: UniqueID, + transaction?: any + ): Promise> { + return this.repository.deleteByIdInCompany(companyId, customerId, transaction); } } diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 5ef9df65..1a3d9ddc 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -10,7 +10,7 @@ import { ListCustomersAssembler, ListCustomersUseCase, } from "../application"; -import { CustomerService } from "../domain"; +import { CustomerService, ICustomerService } from "../domain"; import { CustomerMapper } from "./mappers"; import { CustomerRepository } from "./sequelize"; @@ -39,7 +39,7 @@ type CustomerDeps = { let _repo: CustomerRepository | null = null; let _mapper: CustomerMapper | null = null; -let _service: CustomerService | null = null; +let _service: ICustomerService | null = null; let _assemblers: CustomerDeps["assemblers"] | null = null; export function getCustomerDependencies(params: ModuleParams): CustomerDeps { diff --git a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts index d1a6d24a..eba5887b 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts @@ -23,7 +23,7 @@ export class CreateCustomerController extends ExpressController { dto.customerCompanyId = user.companyId; */ - const result = await this.useCase.execute(dto); + const result = await this.useCase.execute({ tenantId, dto }); return result.match( (data) => this.created(data), diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index 5da1abbc..edef179d 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -1,4 +1,3 @@ -import { enforceTenant } from "@erp/auth/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; @@ -28,7 +27,7 @@ export const customersRouter = (params: ModuleParams) => { const deps = getCustomerDependencies(params); // 🔐 Autenticación + Tenancy para TODO el router - router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); + //router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); router.get( "/", diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts index a3f673a6..ed0792c7 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts @@ -19,9 +19,10 @@ export class CustomerModel extends Model< }*/ declare id: string; + declare company_id: string; declare reference: CreationOptional; - declare is_freelancer: boolean; + declare is_company: boolean; declare name: string; declare trade_name: CreationOptional; declare tin: string; @@ -43,20 +44,28 @@ export class CustomerModel extends Model< declare status: string; declare lang_code: string; declare currency_code: string; + + static associate(database: Sequelize) {} + + static hooks(database: Sequelize) {} } -export default (sequelize: Sequelize) => { +export default (database: Sequelize) => { CustomerModel.init( { id: { type: DataTypes.UUID, primaryKey: true, }, + company_id: { + type: DataTypes.UUID, + allowNull: false, + }, reference: { type: DataTypes.STRING, allowNull: false, }, - is_freelancer: { + is_company: { type: DataTypes.BOOLEAN, allowNull: false, }, @@ -149,7 +158,7 @@ export default (sequelize: Sequelize) => { }, }, { - sequelize, + sequelize: database, tableName: "customers", paranoid: true, // softs deletes @@ -160,8 +169,8 @@ export default (sequelize: Sequelize) => { deletedAt: "deleted_at", indexes: [ + { name: "company_idx", fields: ["company_id"], unique: false }, { name: "email_idx", fields: ["email"], unique: true }, - { name: "reference_idx", fields: ["reference"], unique: true }, ], whereMergeStrategy: "and", // <- cómo tratar el merge de un scope diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts index fe880cd2..00e30820 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts @@ -19,81 +19,108 @@ export class CustomerRepository this.mapper = mapper; } - async existsById(id: UniqueID, transaction?: Transaction): Promise> { + /** + * + * Guarda un nuevo cliente o actualiza uno existente. + * + * @param customer - El cliente a guardar. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async save(customer: Customer, transaction: Transaction): Promise> { try { - const result = await this._exists(CustomerModel, "id", id.toString(), transaction); - - return Result.ok(Boolean(result)); + const data = this.mapper.mapToPersistence(customer); + const [instance] = await CustomerModel.upsert(data, { transaction, returning: true }); + return this.mapper.mapToDomain(instance); } catch (err: unknown) { return Result.fail(errorMapper.toDomainError(err)); } } /** + * Comprueba si existe un Customer con un `id` dentro de una `company`. * - * Persiste una nueva factura o actualiza una existente. - * - * @param invoice - El agregado a guardar. + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param id - Identificador UUID del cliente. * @param transaction - Transacción activa para la operación. - * @returns Result + * @returns Result */ - async save(invoice: Customer, transaction: Transaction): Promise> { + async existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise> { try { - const data = this.mapper.mapToPersistence(invoice); - await CustomerModel.upsert(data, { transaction }); - return Result.ok(invoice); - } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + const count = await CustomerModel.count({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(Boolean(count > 0)); + } catch (error: any) { + return Result.fail(errorMapper.toDomainError(error)); } } /** + * Recupera un cliente por su ID y companyId. * - * Busca una factura por su identificador único. - * @param id - UUID de la factura. + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param id - Identificador UUID del cliente. * @param transaction - Transacción activa para la operación. * @returns Result */ - async findById(id: UniqueID, transaction: Transaction): Promise> { + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise> { try { - const rawData = await this._findById(CustomerModel, id.toString(), { transaction }); + const row = await CustomerModel.findOne({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); - if (!rawData) { - return Result.fail(new Error(`Invoice with id ${id} not found.`)); + if (!row) { + return Result.fail(new Error(`Customer ${id.toString()} not found`)); } - return this.mapper.mapToDomain(rawData); - } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return this.mapper.mapToDomain(row); + } catch (error: any) { + return Result.fail(errorMapper.toDomainError(error)); } } /** + * Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.). * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param criteria - Criterios de búsqueda. * @param transaction - Transacción activa para la operación. - * @returns Result + * @returns Result, Error> * * @see Criteria */ - public async findByCriteria( + async findByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, - transaction: Transaction - ): Promise, Error>> { + transaction?: any + ): Promise>> { try { const converter = new CriteriaToSequelizeConverter(); const query = converter.convert(criteria); - console.debug({ criteria, transaction, query, CustomerModel }); + query.where = { + ...query.where, + company_id: companyId.toString(), + }; + + console.debug({ model: "CustomerModel", criteria, query }); const instances = await CustomerModel.findAll({ ...query, transaction, }); - console.debug(instances); - return this.mapper.mapArrayToDomain(instances); } catch (err: unknown) { console.error(err); @@ -103,16 +130,30 @@ export class CustomerRepository /** * - * Elimina o marca como eliminada una factura. - * @param id - UUID de la factura a eliminar. + * Elimina o marca como eliminado un cliente. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param id - UUID del cliente a eliminar. * @param transaction - Transacción activa para la operación. * @returns Result */ - async deleteById(id: UniqueID, transaction: any): Promise> { + async deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise> { try { - await this._deleteById(CustomerModel, id, false, transaction); + const deleted = await CustomerModel.destroy({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + + if (deleted === 0) { + return Result.fail(new Error(`Customer with id ${id} not found in company ${companyId}.`)); + } return Result.ok(); } catch (err: unknown) { + // , `Error deleting customer ${id} in company ${companyId}` return Result.fail(errorMapper.toDomainError(err)); } } 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 991dc2c3..a6e4679e 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 @@ -2,9 +2,9 @@ import * as z from "zod/v4"; export const CreateCustomerRequestSchema = z.object({ id: z.uuid(), - reference: z.string(), + reference: z.string().optional(), - is_freelancer: z.boolean(), + is_company: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), @@ -22,7 +22,7 @@ export const CreateCustomerRequestSchema = z.object({ legal_record: z.string(), - default_tax: z.number(), + default_tax: z.array(z.string()), status: z.string(), lang_code: z.string(), currency_code: z.string(), diff --git a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts b/modules/customers/src/common/dto/response/customer-creation.result.dto.ts index d5c98fce..104588f3 100644 --- a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts +++ b/modules/customers/src/common/dto/response/customer-creation.result.dto.ts @@ -5,7 +5,7 @@ export const CustomerCreationResponseSchema = z.object({ id: z.uuid(), reference: z.string(), - is_freelancer: z.boolean(), + is_companyr: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), diff --git a/modules/customers/src/common/dto/response/customer-list.response.dto.ts b/modules/customers/src/common/dto/response/customer-list.response.dto.ts index 934f7513..63985c79 100644 --- a/modules/customers/src/common/dto/response/customer-list.response.dto.ts +++ b/modules/customers/src/common/dto/response/customer-list.response.dto.ts @@ -6,7 +6,7 @@ export const CustomerListResponseSchema = createListViewResponseSchema( id: z.uuid(), reference: z.string(), - is_freelancer: z.boolean(), + is_companyr: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), diff --git a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts index 04aac9fc..5c858b8b 100644 --- a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts +++ b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts @@ -5,7 +5,7 @@ export const GetCustomerByIdResponseSchema = z.object({ id: z.uuid(), reference: z.string(), - is_freelancer: z.boolean(), + is_companyr: z.boolean(), name: z.string(), trade_name: z.string(), tin: z.string(), diff --git a/modules/customers/src/web/pages/create/create.tsx b/modules/customers/src/web/pages/create/create.tsx index a9a38a9a..85cf1436 100644 --- a/modules/customers/src/web/pages/create/create.tsx +++ b/modules/customers/src/web/pages/create/create.tsx @@ -1,6 +1,6 @@ -import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; +import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; -import { useBlocker, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation"; import { useTranslation } from "../../i18n"; @@ -9,7 +9,6 @@ import { CustomerEditForm } from "./customer-edit-form"; export const CustomerCreate = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { block, unblock } = useBlocker(1); const { mutate, isPending, isError, error } = useCreateCustomerMutation(); @@ -56,12 +55,17 @@ export const CustomerCreate = () => {
-

{t("pages.create.title")}

-

{t("pages.create.description")}

+

+ {t("pages.create.title")} +

+

+ {t("pages.create.description")} +

-
diff --git a/modules/customers/src/web/pages/create/customer-edit-form.tsx b/modules/customers/src/web/pages/create/customer-edit-form.tsx index 15e93146..11af34b9 100644 --- a/modules/customers/src/web/pages/create/customer-edit-form.tsx +++ b/modules/customers/src/web/pages/create/customer-edit-form.tsx @@ -4,13 +4,18 @@ import { useForm } from "react-hook-form"; import { TaxesMultiSelectField } from "@erp/core/components"; import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components"; import { + Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Form, - Label, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, RadioGroup, RadioGroupItem, } from "@repo/shadcn-ui/components"; @@ -21,10 +26,24 @@ import { CustomerData, CustomerDataFormSchema } from "./customer.schema"; const defaultCustomerData = { id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f", + is_company: true, status: "active", - name: "1", - language_code: "ES", - currency: "EUR", + tin: "B12345678", + name: "Pepe", + trade_name: "Pepe's Shop", + email: "pepe@example.com", + phone: "+34 123 456 789", + website: "https://pepe.com", + fax: "+34 123 456 789", + street: "Calle Falsa 123", + city: "Madrid", + country: "ES", + postal_code: "28080", + state: "Madrid", + lang_code: "es", + currency_code: "EUR", + legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456", + default_tax: ["iva_21", "rec_5_2"], }; interface CustomerFormProps { @@ -71,44 +90,49 @@ export const CustomerEditForm = ({ return (
-
+
{/* Información básica */} - + {t("form_groups.basic_info.title")} {t("form_groups.basic_info.description")} -
- - { - // Usar setValue del form - form.setValue("customer_type", value); - }} - className='flex gap-6' - > -
- - -
-
- - -
-
-
+ ( + + {t("form_fields.customer_type.label")} + + + + + + + + {t("form_fields.customer_type.company")} + + + + + + + + + {t("form_fields.customer_type.individual")} + + + + + + + )} + /> {/* Dirección */} - + {t("form_groups.address.title")} {t("form_groups.address.description")} @@ -214,7 +237,7 @@ export const CustomerEditForm = ({ {/* Contacto */} - + {t("form_groups.contact_info.title")} {t("form_groups.contact_info.description")} @@ -259,7 +282,7 @@ export const CustomerEditForm = ({ {/* Configuraciones Adicionales */} - + {t("form_groups.additional_config.title")} {t("form_groups.additional_config.description")} @@ -320,6 +343,7 @@ export const CustomerEditForm = ({
+ ); diff --git a/modules/customers/tsconfig.json b/modules/customers/tsconfig.json index 4c9e0112..b4a95fde 100644 --- a/modules/customers/tsconfig.json +++ b/modules/customers/tsconfig.json @@ -7,9 +7,9 @@ }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/packages.bak/rdx-verifactu/tsconfig.json b/packages.bak/rdx-verifactu/tsconfig.json index 5f65a300..7c1fcd19 100644 --- a/packages.bak/rdx-verifactu/tsconfig.json +++ b/packages.bak/rdx-verifactu/tsconfig.json @@ -15,6 +15,6 @@ "incremental": false, "declaration": true, "exactOptionalPropertyTypes": true, - "target": "es2020" + "target": "ES2022" } } diff --git a/packages/rdx-ui/src/components/layout/app-sidebar.tsx b/packages/rdx-ui/src/components/layout/app-sidebar.tsx index 4a1fe77a..e08125ae 100644 --- a/packages/rdx-ui/src/components/layout/app-sidebar.tsx +++ b/packages/rdx-ui/src/components/layout/app-sidebar.tsx @@ -120,17 +120,17 @@ const data = { ], navSecondary: [ { - title: "Settings", + title: "Ajustes", url: "#", icon: SettingsIcon, }, { - title: "Get Help", + title: "Soporte", url: "#", icon: HelpCircleIcon, }, { - title: "Search", + title: "Buscar", url: "#", icon: SearchIcon, }, @@ -180,8 +180,8 @@ const data2 = { ], navMain: [ { - title: "Playground", - url: "#", + title: "Clientes", + url: "/customers", icon: SquareTerminal, isActive: true, items: [ @@ -200,8 +200,8 @@ const data2 = { ], }, { - title: "Models", - url: "#", + title: "Facturas de cliente", + url: "/customer-invoices", icon: Bot, items: [ { diff --git a/packages/rdx-ui/src/components/layout/chart-area-interactive.tsx b/packages/rdx-ui/src/components/layout/chart-area-interactive.tsx index 4d87129d..59a89a7d 100644 --- a/packages/rdx-ui/src/components/layout/chart-area-interactive.tsx +++ b/packages/rdx-ui/src/components/layout/chart-area-interactive.tsx @@ -21,7 +21,7 @@ import { ToggleGroup, ToggleGroupItem, } from "@repo/shadcn-ui/components"; -import { useIsMobile } from "@repo/shadcn-ui/hooks"; +import { useIsMobile } from "@repo/shadcn-ui/hooks/"; const chartData = [ { date: "2024-04-01", desktop: 222, mobile: 150 },