Facturas de cliente

This commit is contained in:
David Arranz 2025-09-14 12:50:29 +02:00
parent 4807e51d82
commit d5c6079d26
12 changed files with 233 additions and 148 deletions

View File

@ -1,64 +0,0 @@
/* import {
ApplicationServiceError,
type IApplicationServiceError,
} from "@/contexts/common/application/services/ApplicationServiceError";
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { Result, UniqueID } from "@shared/contexts";
import { NullOr } from "@shared/utilities";
import { ICustomerInvoiceParticipantAddress, ICustomerInvoiceParticipantAddressRepository } from "../../domain";
export const participantAddressFinder = async (
addressId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<ICustomerInvoiceParticipantAddressRepository>
) => {
if (addressId.isNull()) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.INVALID_REQUEST_PARAM,
`Participant address ID required`
)
);
}
const transaction = adapter.startTransaction();
let address: NullOr<ICustomerInvoiceParticipantAddress> = null;
try {
await transaction.complete(async (t) => {
address = await repository({ transaction: t }).getById(addressId);
});
if (address === null) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(ApplicationServiceError.NOT_FOUND_ERROR, "", {
id: addressId.toString(),
entity: "participant address",
})
);
}
return Result.ok<ICustomerInvoiceParticipantAddress>(address);
} catch (error: unknown) {
const _error = error as Error;
if (repository().isRepositoryError(_error)) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.REPOSITORY_ERROR,
_error.message,
_error
)
);
}
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.UNEXCEPTED_ERROR,
_error.message,
_error
)
);
}
};
*/

View File

@ -1,21 +0,0 @@
/* import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { UniqueID } from "@shared/contexts";
import { ICustomerInvoiceParticipantRepository } from "../../domain";
import { CustomerInvoiceCustomer } from "../../domain/entities/customer-invoice-customer/customer-invoice-customer";
export const participantFinder = async (
participantId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<ICustomerInvoiceParticipantRepository>
): Promise<CustomerInvoiceCustomer | undefined> => {
if (!participantId || (participantId && participantId.isNull())) {
return Promise.resolve(undefined);
}
const participant = await adapter
.startTransaction()
.complete((t) => repository({ transaction: t }).getById(participantId));
return Promise.resolve(participant ? participant : undefined);
};
*/

View File

@ -1,11 +1,11 @@
import { JsonTaxCatalogProvider } from "@erp/core"; import { JsonTaxCatalogProvider } from "@erp/core";
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, 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 { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CustomerInvoiceService } from "../../../domain"; import { CustomerInvoiceService } from "../../../domain";
import { CreateCustomerInvoiceAssembler } from "./assembler"; import { CustomerInvoiceFullPresenter } from "../../presenters";
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props"; import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
type CreateCustomerInvoiceUseCaseInput = { type CreateCustomerInvoiceUseCaseInput = {
@ -17,15 +17,19 @@ export class CreateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: CreateCustomerInvoiceAssembler, private readonly presenterRegistry: IPresenterRegistry,
private readonly taxCatalog: JsonTaxCatalogProvider private readonly taxCatalog: JsonTaxCatalogProvider
) {} ) {}
public execute(params: CreateCustomerInvoiceUseCaseInput) { public execute(params: CreateCustomerInvoiceUseCaseInput) {
const { dto, companyId } = params; const { dto, companyId } = params;
const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog }); const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "FULL",
}) as CustomerInvoiceFullPresenter;
// 1) Mapear DTO → props de dominio // 1) Mapear DTO → props de dominio
const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog });
const dtoResult = dtoMapper.map(dto); const dtoResult = dtoMapper.map(dto);
if (dtoResult.isFailure) { if (dtoResult.isFailure) {
return Result.fail(dtoResult.error); return Result.fail(dtoResult.error);
@ -43,6 +47,7 @@ export class CreateCustomerInvoiceUseCase {
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try {
const existsGuard = await this.ensureNotExists(companyId, id, transaction); const existsGuard = await this.ensureNotExists(companyId, id, transaction);
if (existsGuard.isFailure) { if (existsGuard.isFailure) {
return Result.fail(existsGuard.error); return Result.fail(existsGuard.error);
@ -53,9 +58,13 @@ export class CreateCustomerInvoiceUseCase {
return Result.fail(saveResult.error); return Result.fail(saveResult.error);
} }
const viewDTO = this.assembler.toDTO(saveResult.data); const invoice = saveResult.data;
const dto = presenter.toOutput(invoice);
return Result.ok(viewDTO); return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}); });
} }

View File

@ -72,7 +72,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CreateCustomerInvoiceRequestSchema), validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.create(); const useCase = deps.build.create();
const controller = new CreateCustomerInvoiceController(useCase); const controller = new CreateCustomerInvoiceController(useCase);

View File

@ -0,0 +1,21 @@
import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd";
import { CustomerService } from "../../domain";
export class CustomerNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> {
constructor(
private readonly service: CustomerService,
private readonly companyId: UniqueID,
private readonly transaction?: any
) {
super();
}
public async isSatisfiedBy(customerId: UniqueID): Promise<boolean> {
const exists = await this.service.existsByIdInCompany(
customerId,
this.companyId,
this.transaction
);
return !exists;
}
}

View File

@ -0,0 +1 @@
export * from "./customer-not-exists.spec";

View File

@ -1,10 +1,11 @@
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, 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 { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CreateCustomerRequestDTO } from "../../../common"; import { CreateCustomerRequestDTO } from "../../../../common";
import { CustomerService } from "../../domain"; import { CustomerService } from "../../../domain";
import { CreateCustomersAssembler } from "./assembler"; import { CustomerFullPresenter } from "../../presenters";
import { CustomerNotExistsInCompanySpecification } from "../../specs";
import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"; import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props";
type CreateCustomerUseCaseInput = { type CreateCustomerUseCaseInput = {
@ -16,11 +17,15 @@ export class CreateCustomerUseCase {
constructor( constructor(
private readonly service: CustomerService, private readonly service: CustomerService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: CreateCustomersAssembler private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: CreateCustomerUseCaseInput) { public execute(params: CreateCustomerUseCaseInput) {
const { dto, companyId } = params; const { dto, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer",
projection: "FULL",
}) as CustomerFullPresenter;
// 1) Mapear DTO → props de dominio // 1) Mapear DTO → props de dominio
const dtoResult = mapDTOToCreateCustomerProps(dto); const dtoResult = mapDTOToCreateCustomerProps(dto);
@ -40,9 +45,17 @@ export class CreateCustomerUseCase {
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
const existsGuard = await this.ensureNotExists(companyId, id, transaction); try {
if (existsGuard.isFailure) { // Verificar que no exista ya un cliente con el mismo id en la companyId
return Result.fail(existsGuard.error); const spec = new CustomerNotExistsInCompanySpecification(
this.service,
companyId,
transaction
);
const isNew = await spec.isSatisfiedBy(newCustomer.id);
if (!isNew) {
return Result.fail(new DuplicateEntityError("Customer", "id", String(newCustomer.id)));
} }
const saveResult = await this.service.saveCustomer(newCustomer, transaction); const saveResult = await this.service.saveCustomer(newCustomer, transaction);
@ -50,29 +63,13 @@ export class CreateCustomerUseCase {
return Result.fail(saveResult.error); return Result.fail(saveResult.error);
} }
const viewDTO = this.assembler.toDTO(saveResult.data); const customer = saveResult.data;
const dto = presenter.toOutput(customer);
return Result.ok(viewDTO); return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}); });
} }
/**
Verifica que no exista un Customer con el mismo id en la companyId.
*/
private async ensureNotExists(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<void, Error>> {
const existsResult = await this.service.existsByIdInCompany(companyId, id, transaction);
if (existsResult.isFailure) {
return Result.fail(existsResult.error);
}
if (existsResult.data) {
return Result.fail(new DuplicateEntityError("Customer", "id", String(id)));
}
return Result.ok<void>(undefined);
}
} }

View File

@ -5,6 +5,7 @@ import {
SequelizeTransactionManager, SequelizeTransactionManager,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CreateCustomerUseCase,
CustomerFullPresenter, CustomerFullPresenter,
ListCustomersPresenter, ListCustomersPresenter,
ListCustomersUseCase, ListCustomersUseCase,
@ -23,8 +24,8 @@ export type CustomerDeps = {
build: { build: {
list: () => ListCustomersUseCase; list: () => ListCustomersUseCase;
get: () => GetCustomerUseCase; get: () => GetCustomerUseCase;
/*create: () => CreateCustomerUseCase; create: () => CreateCustomerUseCase;
update: () => UpdateCustomerUseCase; /*update: () => UpdateCustomerUseCase;
delete: () => DeleteCustomerUseCase;*/ delete: () => DeleteCustomerUseCase;*/
}; };
}; };
@ -64,9 +65,9 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
service, service,
build: { build: {
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry), list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
/*get: () => new GetCustomerUseCase(_service!, transactionManager!, presenterRegistry!), get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
create: () => new CreateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), /*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/ delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/
}, },
}; };

View File

@ -2,9 +2,17 @@ import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { CustomerListRequestSchema, GetCustomerByIdRequestSchema } from "../../../common/dto"; import {
CreateCustomerRequestSchema,
CustomerListRequestSchema,
GetCustomerByIdRequestSchema,
} from "../../../common/dto";
import { buildCustomerDependencies } from "../dependencies"; import { buildCustomerDependencies } from "../dependencies";
import { GetCustomerController, ListCustomersController } from "./controllers"; import {
CreateCustomerController,
GetCustomerController,
ListCustomersController,
} from "./controllers";
export const customersRouter = (params: ModuleParams) => { export const customersRouter = (params: ModuleParams) => {
const { app, database, baseRoutePath, logger } = params as { const { app, database, baseRoutePath, logger } = params as {
@ -61,7 +69,7 @@ export const customersRouter = (params: ModuleParams) => {
} }
); );
/*router.post( router.post(
"/", "/",
//checkTabContext, //checkTabContext,
@ -73,7 +81,7 @@ export const customersRouter = (params: ModuleParams) => {
} }
); );
router.put( /*router.put(
"/:customer_id", "/:customer_id",
//checkTabContext, //checkTabContext,
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"), validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),

View File

@ -3,4 +3,5 @@ export * from "./aggregate-root-repository.interface";
export * from "./domain-entity"; export * from "./domain-entity";
export * from "./events/domain-event.interface"; export * from "./events/domain-event.interface";
export * from "./helpers"; export * from "./helpers";
export * from "./specification";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -0,0 +1,132 @@
export interface IBaseSpecification<T> {
isSatisfiedBy(candidate: T): Promise<boolean>;
}
export interface ICompositeSpecification<T> extends IBaseSpecification<T> {
and(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
or(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
not(): ICompositeSpecification<T>;
}
export abstract class CompositeSpecification<T> implements ICompositeSpecification<T> {
abstract isSatisfiedBy(candidate: T): Promise<boolean>;
public and(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
return new AndSpecification<T>(this, other);
}
public andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
return new AndNotSpecification<T>(this, other);
}
public or(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
return new OrSpecification<T>(this, other);
}
public orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
return new OrNotSpecification<T>(this, other);
}
public not(): ICompositeSpecification<T> {
return new NotSpecification<T>(this);
}
}
class AndSpecification<T> extends CompositeSpecification<T> {
constructor(
private readonly left: ICompositeSpecification<T>,
private readonly right: ICompositeSpecification<T>
) {
super();
}
public async isSatisfiedBy(candidate: T): Promise<boolean> {
const leftResult = await this.left.isSatisfiedBy(candidate);
if (!leftResult) return false;
const rightResult = await this.right.isSatisfiedBy(candidate);
return rightResult;
}
toString(): string {
return `(${this.left.toString()} and ${this.right.toString()})`;
}
}
class AndNotSpecification<T> extends AndSpecification<T> {
public async isSatisfiedBy(candidate: T): Promise<boolean> {
return (await super.isSatisfiedBy(candidate)) !== true;
}
toString(): string {
return `not ${super.toString()}`;
}
}
class OrSpecification<T> extends CompositeSpecification<T> {
constructor(
private readonly left: ICompositeSpecification<T>,
private readonly right: ICompositeSpecification<T>
) {
super();
}
public async isSatisfiedBy(candidate: T): Promise<boolean> {
const leftResult = await this.left.isSatisfiedBy(candidate);
if (leftResult) return true;
const rightResult = await this.right.isSatisfiedBy(candidate);
return rightResult;
}
toString(): string {
return `(${this.left.toString()} or ${this.right.toString()})`;
}
}
class OrNotSpecification<T> extends OrSpecification<T> {
public async isSatisfiedBy(candidate: T): Promise<boolean> {
return (await super.isSatisfiedBy(candidate)) !== true;
}
toString(): string {
return `not ${super.toString()}`;
}
}
export class NotSpecification<T> extends CompositeSpecification<T> {
constructor(private readonly spec: ICompositeSpecification<T>) {
super();
}
public async isSatisfiedBy(candidate: T): Promise<boolean> {
return !this.spec.isSatisfiedBy(candidate);
}
toString(): string {
return `(not ${this.spec.toString()})`;
}
}
export class RangeSpecification<T> extends CompositeSpecification<T> {
private readonly min: T;
private readonly max: T;
private readonly compareFn: (a: T, b: T) => number;
constructor(min: T, max: T, compareFn: (a: T, b: T) => number) {
super();
this.min = min;
this.max = max;
this.compareFn = compareFn;
}
public async isSatisfiedBy(candidate: T): Promise<boolean> {
return this.compareFn(candidate, this.min) >= 0 && this.compareFn(candidate, this.max) <= 0;
}
toString(): string {
return `range [${this.min}, ${this.max}]`;
}
}