Clientes y facturas de cliente

This commit is contained in:
David Arranz 2025-10-04 18:29:14 +02:00
parent 5534f06f21
commit 41ca0c2286
40 changed files with 304 additions and 183 deletions

View File

@ -8,7 +8,7 @@ Este módulo es para **facturas de cliente (Customer Invoices)** y debe cumplir
- Las **líneas de factura** (`InvoiceLine`) se modelan como entidades o value objects dentro del agregado. - Las **líneas de factura** (`InvoiceLine`) se modelan como entidades o value objects dentro del agregado.
- Se usará un `Mapper` para convertir entre dominio y persistencia. - Se usará un `Mapper` para convertir entre dominio y persistencia.
- Repositorios (`ICustomerInvoiceRepository`) solo manejan agregados. - Repositorios (`ICustomerInvoiceRepository`) solo manejan agregados.
- Operaciones como `createInvoice`, `updateInvoice`, `getInvoiceById` serán gestionadas en `CustomerInvoiceService`. - Operaciones como `createInvoice`, `updateInvoice`, `getInvoiceById` serán gestionadas en `CustomerInvoiceApplicationService`.
**SOLID** **SOLID**
- Usar SRP: cada clase con una responsabilidad clara. - Usar SRP: cada clase con una responsabilidad clara.
@ -74,7 +74,7 @@ La entidad `CustomerInvoice` tendrá:
✅ Los repositorios deben capturar errores de Sequelize (`UniqueConstraintError`, etc.) y convertirlos a errores de dominio con mensajes claros y específicos (mediante `errorMapper`). ✅ Los repositorios deben capturar errores de Sequelize (`UniqueConstraintError`, etc.) y convertirlos a errores de dominio con mensajes claros y específicos (mediante `errorMapper`).
📌 TESTING: 📌 TESTING:
✅ Los servicios (`CustomerInvoiceService`) serán testeados con mocks de repositorio. ✅ Los servicios (`CustomerInvoiceApplicationService`) serán testeados con mocks de repositorio.
✅ Las rutas serán testeadas con `supertest`. ✅ Las rutas serán testeadas con `supertest`.
✅ Las validaciones de ValueObjects tendrán pruebas unitarias. ✅ Las validaciones de ValueObjects tendrán pruebas unitarias.

View File

@ -46,6 +46,7 @@ export function translateSequelizeError(err: unknown): Error {
path: e.path ?? "unknown", path: e.path ?? "unknown",
message: e.message, message: e.message,
// Algunas props útiles: e.validatorKey / e.validatorName // Algunas props útiles: e.validatorKey / e.validatorName
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
rule: (e as any).validatorKey ?? undefined, rule: (e as any).validatorKey ?? undefined,
})); }));
@ -55,7 +56,7 @@ export function translateSequelizeError(err: unknown): Error {
return DomainValidationError.invalidFormat(d.path, d.message, { cause: err }); return DomainValidationError.invalidFormat(d.path, d.message, { cause: err });
} }
return new ValidationErrorCollection("Invalid data provided", details, { cause: err }); return new ValidationErrorCollection(details, { cause: err });
} }
// 4) Conectividad / indisponibilidad (transitorio) // 4) Conectividad / indisponibilidad (transitorio)
@ -71,5 +72,5 @@ export function translateSequelizeError(err: unknown): Error {
// 6) Fallback: deja pasar si ya es un Error tipado de tu app, si no wrap // 6) Fallback: deja pasar si ya es un Error tipado de tu app, si no wrap
if (err instanceof Error) return err; if (err instanceof Error) return err;
return new InfrastructureRepositoryError("Unknown persistence error", { cause: err as any }); return new InfrastructureRepositoryError("Unknown persistence error", { cause: err as unknown });
} }

View File

@ -28,6 +28,6 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src", "../../packages/rdx-ddd/src/helpers/extract-or-push-error.ts"], "include": ["src"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -2,11 +2,15 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CustomerInvoiceListDTO } from "../../infrastructure"; import {
import { CustomerInvoice, CustomerInvoicePatchProps, CustomerInvoiceProps } from "../aggregates"; CustomerInvoice,
import { ICustomerInvoiceRepository } from "../repositories"; CustomerInvoicePatchProps,
CustomerInvoiceProps,
} from "../domain/aggregates";
import { ICustomerInvoiceRepository } from "../domain/repositories";
import { CustomerInvoiceListDTO } from "../infrastructure";
export class CustomerInvoiceService { export class CustomerInvoiceApplicationService {
constructor(private readonly repository: ICustomerInvoiceRepository) {} constructor(private readonly repository: ICustomerInvoiceRepository) {}
/** /**
@ -33,7 +37,7 @@ export class CustomerInvoiceService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación. * @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
*/ */
async createInvoice( async createInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
invoice: CustomerInvoice, invoice: CustomerInvoice,
transaction: Transaction transaction: Transaction
@ -47,14 +51,14 @@ export class CustomerInvoiceService {
} }
/** /**
* Actualiza una nueva factura y devuelve la factura actualizada. * Actualiza una factura existente y devuelve la factura actualizada.
* *
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param invoice - El agregado a guardar. * @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación. * @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
*/ */
async updateInvoice( async updateInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
invoice: CustomerInvoice, invoice: CustomerInvoice,
transaction: Transaction transaction: Transaction
@ -126,27 +130,25 @@ export class CustomerInvoiceService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error. * @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/ */
async updateInvoiceByIdInCompany( async patchInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
changes: CustomerInvoicePatchProps, changes: CustomerInvoicePatchProps,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction); const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction);
if (invoiceResult.isFailure) { if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error); return Result.fail(invoiceResult.error);
} }
const invoice = invoiceResult.data; const updated = invoiceResult.data.update(changes);
const updatedInvoice = invoice.update(changes);
if (updatedInvoice.isFailure) { if (updated.isFailure) {
return Result.fail(updatedInvoice.error); return Result.fail(updated.error);
} }
return Result.ok(updatedInvoice.data); return Result.ok(updated.data);
} }
/** /**
@ -161,7 +163,7 @@ export class CustomerInvoiceService {
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<void, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction); return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
} }
} }

View File

@ -4,7 +4,7 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CustomerInvoiceService } from "../../../domain"; import { CustomerInvoiceApplicationService } from "../../../domain";
import { CustomerInvoiceFullPresenter } from "../../presenters"; import { CustomerInvoiceFullPresenter } from "../../presenters";
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props"; import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
@ -15,7 +15,7 @@ type CreateCustomerInvoiceUseCaseInput = {
export class CreateCustomerInvoiceUseCase { export class CreateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry, private readonly presenterRegistry: IPresenterRegistry,
private readonly taxCatalog: JsonTaxCatalogProvider private readonly taxCatalog: JsonTaxCatalogProvider

View File

@ -1,7 +1,7 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceApplicationService } from "../../application";
type DeleteCustomerInvoiceUseCaseInput = { type DeleteCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -10,7 +10,7 @@ type DeleteCustomerInvoiceUseCaseInput = {
export class DeleteCustomerInvoiceUseCase { export class DeleteCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}

View File

@ -1,7 +1,7 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceApplicationService } from "../../application";
import { CustomerInvoiceFullPresenter } from "../presenters/domain"; import { CustomerInvoiceFullPresenter } from "../presenters/domain";
type GetCustomerInvoiceUseCaseInput = { type GetCustomerInvoiceUseCaseInput = {
@ -11,7 +11,7 @@ type GetCustomerInvoiceUseCaseInput = {
export class GetCustomerInvoiceUseCase { export class GetCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -1,7 +1,7 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceNumber, CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceApplicationService, CustomerInvoiceNumber } from "../../domain";
import { StatusInvoiceIsApprovedSpecification } from "../specs"; import { StatusInvoiceIsApprovedSpecification } from "../specs";
type IssueCustomerInvoiceUseCaseInput = { type IssueCustomerInvoiceUseCaseInput = {
@ -11,7 +11,7 @@ type IssueCustomerInvoiceUseCaseInput = {
export class IssueCustomerInvoiceUseCase { export class IssueCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}

View File

@ -4,7 +4,7 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto"; import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceApplicationService } from "../../application";
import { ListCustomerInvoicesPresenter } from "../presenters"; import { ListCustomerInvoicesPresenter } from "../presenters";
type ListCustomerInvoicesUseCaseInput = { type ListCustomerInvoicesUseCaseInput = {
@ -14,7 +14,7 @@ type ListCustomerInvoicesUseCaseInput = {
export class ListCustomerInvoicesUseCase { export class ListCustomerInvoicesUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -1,7 +1,7 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceService } from "../../../domain"; import { CustomerInvoiceApplicationService } from "../../../domain";
type DeleteCustomerInvoiceUseCaseInput = { type DeleteCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -10,7 +10,7 @@ type DeleteCustomerInvoiceUseCaseInput = {
export class DeleteCustomerInvoiceUseCase { export class DeleteCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}

View File

@ -1,7 +1,7 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceService } from "../../../domain"; import { CustomerInvoiceApplicationService } from "../../../domain";
import { CustomerInvoiceReportPDFPresenter } from "./reporter"; import { CustomerInvoiceReportPDFPresenter } from "./reporter";
type ReportCustomerInvoiceUseCaseInput = { type ReportCustomerInvoiceUseCaseInput = {
@ -11,7 +11,7 @@ type ReportCustomerInvoiceUseCaseInput = {
export class ReportCustomerInvoiceUseCase { export class ReportCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -3,7 +3,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common"; import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
import { CustomerInvoicePatchProps, CustomerInvoiceService } from "../../../domain"; import { CustomerInvoicePatchProps } from "../../../domain";
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
import { CustomerInvoiceFullPresenter } from "../../presenters"; import { CustomerInvoiceFullPresenter } from "../../presenters";
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
@ -15,7 +16,7 @@ type UpdateCustomerInvoiceUseCaseInput = {
export class UpdateCustomerInvoiceUseCase { export class UpdateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
@ -44,7 +45,7 @@ export class UpdateCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const updatedInvoice = await this.service.updateInvoiceByIdInCompany( const updatedInvoice = await this.service.patchInvoiceByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
patchProps, patchProps,
@ -55,7 +56,7 @@ export class UpdateCustomerInvoiceUseCase {
return Result.fail(updatedInvoice.error); return Result.fail(updatedInvoice.error);
} }
const invoiceOrError = await this.service.updateInvoice( const invoiceOrError = await this.service.updateInvoiceInCompany(
companyId, companyId,
updatedInvoice.data, updatedInvoice.data,
transaction transaction

View File

@ -7,3 +7,7 @@ import { DomainError } from "@repo/rdx-ddd";
export class CustomerInvoiceIdAlreadyExistsError extends DomainError { export class CustomerInvoiceIdAlreadyExistsError extends DomainError {
public readonly code = "DUPLICATE_INVOICE_ID" as const; public readonly code = "DUPLICATE_INVOICE_ID" as const;
} }
export const isCustomerInvoiceIdAlreadyExistsError = (
e: unknown
): e is CustomerInvoiceIdAlreadyExistsError => e instanceof CustomerInvoiceIdAlreadyExistsError;

View File

@ -2,5 +2,4 @@ export * from "./aggregates";
export * from "./entities"; export * from "./entities";
export * from "./errors"; export * from "./errors";
export * from "./repositories"; export * from "./repositories";
export * from "./services";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -79,5 +79,5 @@ export interface ICustomerInvoiceRepository {
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: unknown transaction: unknown
): Promise<Result<void, Error>>; ): Promise<Result<boolean, Error>>;
} }

View File

@ -1 +0,0 @@
export * from "./customer-invoice.service";

View File

@ -24,8 +24,10 @@ import {
} from "../application"; } from "../application";
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core"; import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
import { CustomerInvoiceItemsReportPersenter } from "../application/presenters/queries/customer-invoice-items.report.presenter"; import {
import { CustomerInvoiceService } from "../domain"; CustomerInvoiceApplicationService,
CustomerInvoiceItemsReportPersenter,
} from "../application";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize"; import { CustomerInvoiceRepository } from "./sequelize";
@ -34,7 +36,7 @@ export type CustomerInvoiceDeps = {
mapperRegistry: IMapperRegistry; mapperRegistry: IMapperRegistry;
presenterRegistry: IPresenterRegistry; presenterRegistry: IPresenterRegistry;
repo: CustomerInvoiceRepository; repo: CustomerInvoiceRepository;
service: CustomerInvoiceService; service: CustomerInvoiceApplicationService;
catalogs: { catalogs: {
taxes: JsonTaxCatalogProvider; taxes: JsonTaxCatalogProvider;
}; };
@ -71,7 +73,7 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
// Repository & Services // Repository & Services
const repo = new CustomerInvoiceRepository({ mapperRegistry, database }); const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
const service = new CustomerInvoiceService(repo); const service = new CustomerInvoiceApplicationService(repo);
// Presenter Registry // Presenter Registry
const presenterRegistry = new InMemoryPresenterRegistry(); const presenterRegistry = new InMemoryPresenterRegistry();

View File

@ -2,13 +2,15 @@
// (si defines un error más ubicuo dentro del BC con su propia clase) // (si defines un error más ubicuo dentro del BC con su propia clase)
import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api"; import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api";
import { CustomerInvoiceIdAlreadyExistsError } from "../../domain"; import {
CustomerInvoiceIdAlreadyExistsError,
isCustomerInvoiceIdAlreadyExistsError,
} from "../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
const invoiceDuplicateRule: ErrorToApiRule = { const invoiceDuplicateRule: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e): e is CustomerInvoiceIdAlreadyExistsError => matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e),
e instanceof CustomerInvoiceIdAlreadyExistsError,
build: (e) => build: (e) =>
new ConflictApiError( new ConflictApiError(
(e as CustomerInvoiceIdAlreadyExistsError).message || (e as CustomerInvoiceIdAlreadyExistsError).message ||

View File

@ -162,13 +162,13 @@ export class CustomerInvoiceRepository
id: UniqueID, id: UniqueID,
transaction: Transaction transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const { CustomerModel } = this._database.models;
try { try {
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
resource: "customer-invoice", resource: "customer-invoice",
}); });
const { CustomerModel } = this._database.models;
const row = await CustomerInvoiceModel.findOne({ const row = await CustomerInvoiceModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() }, where: { id: id.toString(), company_id: companyId.toString() },
order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]], order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]],
@ -203,8 +203,8 @@ export class CustomerInvoiceRepository
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
} }
const customer = mapper.mapToDomain(row); const invoice = mapper.mapToDomain(row);
return customer; return invoice;
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
@ -226,12 +226,14 @@ export class CustomerInvoiceRepository
criteria: Criteria, criteria: Criteria,
transaction: Transaction transaction: Transaction
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> { ): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
const { CustomerModel } = this._database.models;
try { try {
const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({ const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({
resource: "customer-invoice", resource: "customer-invoice",
query: "LIST", query: "LIST",
}); });
const { CustomerModel } = this._database.models;
const converter = new CriteriaToSequelizeConverter(); const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria); const query = converter.convert(criteria);
@ -272,20 +274,24 @@ export class CustomerInvoiceRepository
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param id - UUID de la factura a eliminar. * @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<boolean, Error>
*/ */
async deleteByIdInCompany( async deleteByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: Transaction transaction: Transaction
): Promise<Result<void, Error>> { ): Promise<Result<boolean, Error>> {
try { try {
const deleted = await CustomerInvoiceModel.destroy({ const deleted = await CustomerInvoiceModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() }, where: { id: id.toString(), company_id: companyId.toString() },
transaction, transaction,
}); });
return Result.ok<void>(); if (deleted === 0) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
}
return Result.ok(true);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }

View File

@ -45,7 +45,7 @@ import { PropsWithChildren, createContext } from "react";
export type CustomerInvoicesContextType = {}; export type CustomerInvoicesContextType = {};
export type CustomerInvoicesContextParamsType = { export type CustomerInvoicesContextParamsType = {
//service: CustomerInvoiceService; //service: CustomerInvoiceApplicationService;
}; };
export const CustomerInvoicesContext = createContext<CustomerInvoicesContextType>({}); export const CustomerInvoicesContext = createContext<CustomerInvoicesContextType>({});

View File

@ -1,93 +1,55 @@
// application/customer-application-service.ts
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { CustomerListDTO } from "../../infrastructure"; import { Transaction } from "sequelize";
import { Customer, CustomerPatchProps, CustomerProps } from "../aggregates"; import { Customer, CustomerPatchProps, ICustomerRepository } from "../domain";
import { ICustomerRepository } from "../repositories"; import { CustomerListDTO } from "../infrastructure";
export class CustomerService { export class CustomerApplicationService {
constructor(private readonly repository: ICustomerRepository) {} constructor(private readonly repository: ICustomerRepository) {}
/** /**
* Construye un nuevo agregado Customer a partir de props validadas. * Guarda un nuevo cliente y devuelve el cliente guardado.
*
* @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<Customer, Error> - El agregado construido o un error si falla la creación.
*/
buildCustomerInCompany(
companyId: UniqueID,
props: Omit<CustomerProps, "companyId">,
customerId?: UniqueID
): Result<Customer, Error> {
return Customer.create({ ...props, companyId }, customerId);
}
/**
* Guarda una instancia de Customer en persistencia.
*
* @param customer - El agregado a guardar (con el companyId ya asignado)
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
*/
async saveCustomer(customer: Customer, transaction: unknown): Promise<Result<Customer, Error>> {
return this.repository.save(customer, transaction);
}
/**
*
* Comprueba si existe o no en persistencia un cliente con el ID proporcionado
* *
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param customerId - Identificador UUID del cliente * @param customer - El cliente a guardar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe el cliente o no. * @returns Result<Customer, Error> - El cliente guardado o un error si falla la operación.
*/ */
async createCustomerInCompany(
existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customer: Customer,
transaction?: unknown transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<Customer, Error>> {
return this.repository.existsByIdInCompany(companyId, customerId, transaction); const result = await this.repository.create(customer, transaction);
if (result.isFailure) return Result.fail(result.error);
return this.getCustomerByIdInCompany(companyId, customer.id, transaction);
} }
/** /**
* Obtiene una colección de clientes que cumplen con los filtros definidos en un objeto Criteria. * Actualiza un cliente existente y devuelve el cliente actualizado.
* *
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @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 customer - El cliente a guardar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Collection<Customer>, Error> - Colección de clientes o error. * @returns Result<Customer, Error> - El cliente guardado o un error si falla la operación.
*/ */
async findCustomerByCriteriaInCompany( async updateCustomerInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, customer: Customer,
transaction?: unknown transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>> { ): Promise<Result<Customer, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); const result = await this.repository.update(customer, transaction);
} if (result.isFailure) return Result.fail(result.error);
/** return this.getCustomerByIdInCompany(companyId, customer.id, transaction);
* Recupera un cliente por su identificador único.
*
* @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<Customer, Error> - Cliente encontradoF o error.
*/
async getCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: unknown
): Promise<Result<Customer>> {
return this.repository.getByIdInCompany(companyId, customerId, transaction);
} }
/** /**
* Actualiza parcialmente un cliente existente con nuevos datos. * Actualiza parcialmente un cliente existente con nuevos datos.
* No lo guarda en el repositorio. * Solo en memoria. No lo guarda en el repositorio.
* *
* @param companyId - Identificador de la empresa a la que pertenece el cliente. * @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param customerId - Identificador del cliente a actualizar. * @param customerId - Identificador del cliente a actualizar.
@ -95,26 +57,23 @@ export class CustomerService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - Cliente actualizado o error. * @returns Result<Customer, Error> - Cliente actualizado o error.
*/ */
async updateCustomerByIdInCompany( async patchCustomerByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
changes: CustomerPatchProps, changes: CustomerPatchProps,
transaction?: unknown transaction?: Transaction
): Promise<Result<Customer, Error>> { ): Promise<Result<Customer, Error>> {
const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction); const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction);
if (customerResult.isFailure) { if (customerResult.isFailure) {
return Result.fail(customerResult.error); return Result.fail(customerResult.error);
} }
const customer = customerResult.data; const updated = customerResult.data.update(changes);
const updatedCustomer = customer.update(changes); if (updated.isFailure) {
return Result.fail(updated.error);
if (updatedCustomer.isFailure) {
return Result.fail(updatedCustomer.error);
} }
return Result.ok(updatedCustomer.data); return Result.ok(updated.data);
} }
/** /**
@ -128,8 +87,57 @@ export class CustomerService {
async deleteCustomerByIdInCompany( async deleteCustomerByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
transaction?: unknown transaction?: Transaction
): Promise<Result<void>> { ): Promise<Result<boolean, Error>> {
return this.repository.deleteByIdInCompany(companyId, customerId, transaction); return this.repository.deleteByIdInCompany(companyId, customerId, transaction);
} }
/**
*
* Comprueba si existe o no en persistencia un cliente con el ID proporcionado
*
* @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<Boolean, Error> - Existe el cliente o no.
*/
async existsByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
}
/**
* Recupera un cliente por su identificador único.
*
* @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<Customer, Error> - Cliente encontrado o error.
*/
async getCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
return this.repository.getByIdInCompany(companyId, customerId, transaction);
}
/**
* 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<Collection<Customer>, Error> - Colección de clientes o error.
*/
async findCustomerByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
} }

View File

@ -1,2 +1,3 @@
export * from "./customer-application.service";
export * from "./presenters"; export * from "./presenters";
export * from "./use-cases"; export * from "./use-cases";

View File

@ -1,10 +1,10 @@
import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd"; import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd";
import { CustomerService } from "../../domain"; import { CustomerApplicationService } from "../../application";
import { logger } from "../../helpers"; import { logger } from "../../helpers";
export class CustomerNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> { export class CustomerNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly companyId: UniqueID, private readonly companyId: UniqueID,
private readonly transaction?: unknown private readonly transaction?: unknown
) { ) {

View File

@ -4,7 +4,7 @@ import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CreateCustomerRequestDTO } from "../../../../common"; import { CreateCustomerRequestDTO } from "../../../../common";
import { logger } from "../../..//helpers"; import { logger } from "../../..//helpers";
import { CustomerService } from "../../../domain"; import { CustomerApplicationService } from "../../../domain";
import { CustomerFullPresenter } from "../../presenters"; import { CustomerFullPresenter } from "../../presenters";
import { CustomerNotExistsInCompanySpecification } from "../../specs"; import { CustomerNotExistsInCompanySpecification } from "../../specs";
import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"; import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props";
@ -16,7 +16,7 @@ type CreateCustomerUseCaseInput = {
export class CreateCustomerUseCase { export class CreateCustomerUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -1,7 +1,7 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerService } from "../../domain"; import { CustomerApplicationService } from "../../application";
type DeleteCustomerUseCaseInput = { type DeleteCustomerUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -10,7 +10,7 @@ type DeleteCustomerUseCaseInput = {
export class DeleteCustomerUseCase { export class DeleteCustomerUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}

View File

@ -1,7 +1,7 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerService } from "../../domain"; import { CustomerApplicationService } from "../../application";
import { CustomerFullPresenter } from "../presenters"; import { CustomerFullPresenter } from "../presenters";
type GetCustomerUseCaseInput = { type GetCustomerUseCaseInput = {
@ -11,7 +11,7 @@ type GetCustomerUseCaseInput = {
export class GetCustomerUseCase { export class GetCustomerUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -4,7 +4,7 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ListCustomersResponseDTO } from "../../../common/dto"; import { ListCustomersResponseDTO } from "../../../common/dto";
import { CustomerService } from "../../domain"; import { CustomerApplicationService } from "../../application";
import { ListCustomersPresenter } from "../presenters"; import { ListCustomersPresenter } from "../presenters";
type ListCustomersUseCaseInput = { type ListCustomersUseCaseInput = {
@ -14,7 +14,7 @@ type ListCustomersUseCaseInput = {
export class ListCustomersUseCase { export class ListCustomersUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}

View File

@ -3,7 +3,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
import { CustomerPatchProps, CustomerService } from "../../../domain"; import { CustomerPatchProps } from "../../../domain";
import { CustomerApplicationService } from "../../customer-application.service";
import { CustomerFullPresenter } from "../../presenters"; import { CustomerFullPresenter } from "../../presenters";
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props"; import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
@ -15,7 +16,7 @@ type UpdateCustomerUseCaseInput = {
export class UpdateCustomerUseCase { export class UpdateCustomerUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
@ -44,7 +45,7 @@ export class UpdateCustomerUseCase {
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const updatedCustomer = await this.service.updateCustomerByIdInCompany( const updatedCustomer = await this.service.patchCustomerByIdInCompany(
companyId, companyId,
customerId, customerId,
patchProps, patchProps,
@ -55,7 +56,11 @@ export class UpdateCustomerUseCase {
return Result.fail(updatedCustomer.error); return Result.fail(updatedCustomer.error);
} }
const customerOrError = await this.service.saveCustomer(updatedCustomer.data, transaction); const customerOrError = await this.service.updateCustomerInCompany(
companyId,
updatedCustomer.data,
transaction
);
const customer = customerOrError.data; const customer = customerOrError.data;
const dto = presenter.toOutput(customer); const dto = presenter.toOutput(customer);
return Result.ok(dto); return Result.ok(dto);

View File

@ -0,0 +1,8 @@
import { DomainError } from "@repo/rdx-ddd";
export class CustomerNotFoundError extends DomainError {
public readonly code = "CUSTOMER_ID" as const;
}
export const isCustomerNotFoundError = (e: unknown): e is CustomerNotFoundError =>
e instanceof CustomerNotFoundError;

View File

@ -0,0 +1 @@
export * from "./customer-not-found-error";

View File

@ -1,4 +1,4 @@
export * from "./aggregates"; export * from "./aggregates";
export * from "./errors";
export * from "./repositories"; export * from "./repositories";
export * from "./services";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -10,10 +10,23 @@ import { Customer } from "../aggregates";
*/ */
export interface ICustomerRepository { export interface ICustomerRepository {
/** /**
* Guarda (crea o actualiza) un Customer en la base de datos. *
* Retorna el objeto actualizado tras la operación. * Crea un nuevo cliente
*
* @param customer - El cliente nuevo a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/ */
save(customer: Customer, transaction: unknown): Promise<Result<Customer, Error>>; create(customer: Customer, transaction: unknown): Promise<Result<void, Error>>;
/**
* Actualiza un cliente existente.
*
* @param customer - El cliente a actualizar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
update(customer: Customer, transaction: unknown): Promise<Result<void, Error>>;
/** /**
* Comprueba si existe un Customer con un `id` dentro de una `company`. * Comprueba si existe un Customer con un `id` dentro de una `company`.
@ -21,7 +34,7 @@ export interface ICustomerRepository {
existsByIdInCompany( existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction?: unknown transaction: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
/** /**
@ -31,7 +44,7 @@ export interface ICustomerRepository {
getByIdInCompany( getByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction?: unknown transaction: unknown
): Promise<Result<Customer, Error>>; ): Promise<Result<Customer, Error>>;
/** /**
@ -42,7 +55,7 @@ export interface ICustomerRepository {
findByCriteriaInCompany( findByCriteriaInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: unknown transaction: unknown
): Promise<Result<Collection<CustomerListDTO>, Error>>; ): Promise<Result<Collection<CustomerListDTO>, Error>>;
/** /**
@ -54,5 +67,5 @@ export interface ICustomerRepository {
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: unknown transaction: unknown
): Promise<Result<void, Error>>; ): Promise<Result<boolean, Error>>;
} }

View File

@ -1 +0,0 @@
export * from "./customer.service";

View File

@ -6,13 +6,13 @@ import {
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CreateCustomerUseCase, CreateCustomerUseCase,
CustomerApplicationService,
CustomerFullPresenter, CustomerFullPresenter,
GetCustomerUseCase,
ListCustomersPresenter, ListCustomersPresenter,
ListCustomersUseCase, ListCustomersUseCase,
UpdateCustomerUseCase, UpdateCustomerUseCase,
} from "../application"; } from "../application";
import { GetCustomerUseCase } from "../application/use-cases/get-customer.use-case";
import { CustomerService } from "../domain";
import { CustomerDomainMapper, CustomerListMapper } from "./mappers"; import { CustomerDomainMapper, CustomerListMapper } from "./mappers";
import { CustomerRepository } from "./sequelize"; import { CustomerRepository } from "./sequelize";
@ -21,7 +21,7 @@ export type CustomerDeps = {
mapperRegistry: IMapperRegistry; mapperRegistry: IMapperRegistry;
presenterRegistry: IPresenterRegistry; presenterRegistry: IPresenterRegistry;
repo: CustomerRepository; repo: CustomerRepository;
service: CustomerService; service: CustomerApplicationService;
build: { build: {
list: () => ListCustomersUseCase; list: () => ListCustomersUseCase;
get: () => GetCustomerUseCase; get: () => GetCustomerUseCase;
@ -32,7 +32,7 @@ export type CustomerDeps = {
}; };
export function buildCustomerDependencies(params: ModuleParams): CustomerDeps { export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
const { database, logger } = params; const { database } = params;
const transactionManager = new SequelizeTransactionManager(database); const transactionManager = new SequelizeTransactionManager(database);
// Mapper Registry // Mapper Registry
@ -43,7 +43,7 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
// Repository & Services // Repository & Services
const repo = new CustomerRepository({ mapperRegistry, database }); const repo = new CustomerRepository({ mapperRegistry, database });
const service = new CustomerService(repo); const service = new CustomerApplicationService(repo);
// Presenter Registry // Presenter Registry
const presenterRegistry = new InMemoryPresenterRegistry(); const presenterRegistry = new InMemoryPresenterRegistry();

View File

@ -0,0 +1,16 @@
import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api";
import { CustomerNotFoundError, isCustomerNotFoundError } from "../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
const customerNotFoundRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isCustomerNotFoundError(e),
build: (e) =>
new ConflictApiError(
(e as CustomerNotFoundError).message || "Customer with the provided id not exists."
),
};
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const customersApiErrorMapper: ApiErrorMapper =
ApiErrorMapper.default().register(customerNotFoundRule);

View File

@ -1,4 +1,9 @@
import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import {
EntityNotFoundError,
InfrastructureRepositoryError,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
@ -13,34 +18,69 @@ export class CustomerRepository
{ {
/** /**
* *
* Guarda un nuevo cliente o actualiza uno existente. * Crea un nuevo cliente
* *
* @param customer - El cliente a guardar. * @param customer - El cliente nuevo a guardar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> * @returns Result<void, Error>
*/ */
async save(customer: Customer, transaction: Transaction): Promise<Result<Customer, Error>> { async create(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
try { try {
const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({ const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({
resource: "customer", resource: "customer",
}); });
const dto = mapper.mapToPersistence(customer);
const mapperData = mapper.mapToPersistence(customer); if (dto.isFailure) {
return Result.fail(dto.error);
if (mapperData.isFailure) {
return Result.fail(mapperData.error);
} }
const { data } = mapperData; const { data } = dto;
const [instance] = await CustomerModel.upsert(data, { transaction, returning: true }); await CustomerModel.create(data, {
const savedCustomer = mapper.mapToDomain(instance); include: [{ all: true }],
return savedCustomer; transaction,
});
return Result.ok();
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
} }
/**
* Actualiza un cliente existente.
*
* @param customer - El cliente a actualizar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
async update(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
try {
const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({
resource: "customer-invoice",
});
const dto = mapper.mapToPersistence(customer);
const { id, ...updatePayload } = dto.data;
const [affected] = await CustomerModel.update(updatePayload, {
where: { id /*, version */ },
//fields: Object.keys(updatePayload),
transaction,
individualHooks: true,
});
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or not found update customer")
);
}
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/** /**
* Comprueba si existe un Customer con un `id` dentro de una `company`. * Comprueba si existe un Customer con un `id` dentro de una `company`.
* *
@ -60,7 +100,7 @@ export class CustomerRepository
transaction, transaction,
}); });
return Result.ok(Boolean(count > 0)); return Result.ok(Boolean(count > 0));
} catch (error: any) { } catch (error: unknown) {
return Result.fail(translateSequelizeError(error)); return Result.fail(translateSequelizeError(error));
} }
} }
@ -94,7 +134,7 @@ export class CustomerRepository
const customer = mapper.mapToDomain(row); const customer = mapper.mapToDomain(row);
return customer; return customer;
} catch (error: any) { } catch (error: unknown) {
return Result.fail(translateSequelizeError(error)); return Result.fail(translateSequelizeError(error));
} }
} }
@ -138,8 +178,6 @@ export class CustomerRepository
company_id: companyId.toString(), company_id: companyId.toString(),
}; };
console.log(query);
const { rows, count } = await CustomerModel.findAndCountAll({ const { rows, count } = await CustomerModel.findAndCountAll({
...query, ...query,
transaction, transaction,
@ -158,20 +196,24 @@ export class CustomerRepository
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param id - UUID del cliente a eliminar. * @param id - UUID del cliente a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<boolean, Error>
*/ */
async deleteByIdInCompany( async deleteByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction: Transaction transaction: Transaction
): Promise<Result<void>> { ): Promise<Result<boolean, Error>> {
try { try {
const deleted = await CustomerModel.destroy({ const deleted = await CustomerModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() }, where: { id: id.toString(), company_id: companyId.toString() },
transaction, transaction,
}); });
return Result.ok<void>(); if (deleted === 0) {
return Result.fail(new EntityNotFoundError("Customer", "id", id.toString()));
}
return Result.ok(true);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }

View File

@ -45,7 +45,7 @@ import { PropsWithChildren, createContext } from "react";
export type CustomersContextType = {}; export type CustomersContextType = {};
export type CustomersContextParamsType = { export type CustomersContextParamsType = {
//service: CustomerService; //service: CustomerApplicationService;
}; };
export const CustomersContext = createContext<CustomersContextType>({}); export const CustomersContext = createContext<CustomersContextType>({});

View File

@ -11,3 +11,6 @@ export class ApplicationError extends BaseError<"application"> {
super("ApplicationError", message, code, options); super("ApplicationError", message, code, options);
} }
} }
export const isApplicationError = (e: unknown): e is ApplicationError =>
e instanceof ApplicationError;

View File

@ -19,3 +19,5 @@ export class DomainError extends BaseError<"domain"> {
super("DomainError", message, code, options); super("DomainError", message, code, options);
} }
} }
export const isDomainError = (e: unknown): e is DomainError => e instanceof DomainError;

View File

@ -3,7 +3,14 @@ import { BaseError } from "./base-error";
/** Errores de infraestructura: DB, red, serialización, proveedores externos */ /** Errores de infraestructura: DB, red, serialización, proveedores externos */
export class InfrastructureError extends BaseError<"infrastructure"> { export class InfrastructureError extends BaseError<"infrastructure"> {
public readonly layer = "infrastructure" as const; public readonly layer = "infrastructure" as const;
constructor(message: string, code = "INFRASTRUCTURE_ERROR", options?: ErrorOptions & { metadata?: Record<string, unknown> }) { constructor(
message: string,
code = "INFRASTRUCTURE_ERROR",
options?: ErrorOptions & { metadata?: Record<string, unknown> }
) {
super("InfrastructureError", message, code, options); super("InfrastructureError", message, code, options);
} }
} }
export const isInfrastructureError = (e: unknown): e is InfrastructureError =>
e instanceof InfrastructureError;