Facturas de cliente
This commit is contained in:
parent
4807e51d82
commit
d5c6079d26
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
*/
|
||||
@ -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);
|
||||
};
|
||||
*/
|
||||
@ -1,11 +1,11 @@
|
||||
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 { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||
import { CustomerInvoiceService } from "../../../domain";
|
||||
import { CreateCustomerInvoiceAssembler } from "./assembler";
|
||||
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
|
||||
|
||||
type CreateCustomerInvoiceUseCaseInput = {
|
||||
@ -17,15 +17,19 @@ export class CreateCustomerInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly assembler: CreateCustomerInvoiceAssembler,
|
||||
private readonly presenterRegistry: IPresenterRegistry,
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider
|
||||
) {}
|
||||
|
||||
public execute(params: CreateCustomerInvoiceUseCaseInput) {
|
||||
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
|
||||
const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog });
|
||||
const dtoResult = dtoMapper.map(dto);
|
||||
if (dtoResult.isFailure) {
|
||||
return Result.fail(dtoResult.error);
|
||||
@ -43,19 +47,24 @@ export class CreateCustomerInvoiceUseCase {
|
||||
|
||||
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
|
||||
if (existsGuard.isFailure) {
|
||||
return Result.fail(existsGuard.error);
|
||||
try {
|
||||
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
|
||||
if (existsGuard.isFailure) {
|
||||
return Result.fail(existsGuard.error);
|
||||
}
|
||||
|
||||
const saveResult = await this.service.saveInvoice(newInvoice, transaction);
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
const invoice = saveResult.data;
|
||||
const dto = presenter.toOutput(invoice);
|
||||
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
const saveResult = await this.service.saveInvoice(newInvoice, transaction);
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
const viewDTO = this.assembler.toDTO(saveResult.data);
|
||||
|
||||
return Result.ok(viewDTO);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
"/",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(CreateCustomerInvoiceRequestSchema),
|
||||
validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.create();
|
||||
const controller = new CreateCustomerInvoiceController(useCase);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
1
modules/customers/src/api/application/specs/index.ts
Normal file
1
modules/customers/src/api/application/specs/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./customer-not-exists.spec";
|
||||
@ -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 { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CreateCustomerRequestDTO } from "../../../common";
|
||||
import { CustomerService } from "../../domain";
|
||||
import { CreateCustomersAssembler } from "./assembler";
|
||||
import { CreateCustomerRequestDTO } from "../../../../common";
|
||||
import { CustomerService } from "../../../domain";
|
||||
import { CustomerFullPresenter } from "../../presenters";
|
||||
import { CustomerNotExistsInCompanySpecification } from "../../specs";
|
||||
import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props";
|
||||
|
||||
type CreateCustomerUseCaseInput = {
|
||||
@ -16,11 +17,15 @@ export class CreateCustomerUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly assembler: CreateCustomersAssembler
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public execute(params: CreateCustomerUseCaseInput) {
|
||||
const { dto, companyId } = params;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer",
|
||||
projection: "FULL",
|
||||
}) as CustomerFullPresenter;
|
||||
|
||||
// 1) Mapear DTO → props de dominio
|
||||
const dtoResult = mapDTOToCreateCustomerProps(dto);
|
||||
@ -40,39 +45,31 @@ export class CreateCustomerUseCase {
|
||||
|
||||
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
|
||||
if (existsGuard.isFailure) {
|
||||
return Result.fail(existsGuard.error);
|
||||
try {
|
||||
// Verificar que no exista ya un cliente con el mismo id en la companyId
|
||||
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);
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
const customer = saveResult.data;
|
||||
const dto = presenter.toOutput(customer);
|
||||
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
const saveResult = await this.service.saveCustomer(newCustomer, transaction);
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
const viewDTO = this.assembler.toDTO(saveResult.data);
|
||||
|
||||
return Result.ok(viewDTO);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
SequelizeTransactionManager,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
CreateCustomerUseCase,
|
||||
CustomerFullPresenter,
|
||||
ListCustomersPresenter,
|
||||
ListCustomersUseCase,
|
||||
@ -23,8 +24,8 @@ export type CustomerDeps = {
|
||||
build: {
|
||||
list: () => ListCustomersUseCase;
|
||||
get: () => GetCustomerUseCase;
|
||||
/*create: () => CreateCustomerUseCase;
|
||||
update: () => UpdateCustomerUseCase;
|
||||
create: () => CreateCustomerUseCase;
|
||||
/*update: () => UpdateCustomerUseCase;
|
||||
delete: () => DeleteCustomerUseCase;*/
|
||||
};
|
||||
};
|
||||
@ -64,9 +65,9 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
|
||||
service,
|
||||
build: {
|
||||
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
|
||||
/*get: () => new GetCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||
create: () => new CreateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||
update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||
get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||
create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||
/*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,9 +2,17 @@ import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth
|
||||
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
|
||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||
import { Sequelize } from "sequelize";
|
||||
import { CustomerListRequestSchema, GetCustomerByIdRequestSchema } from "../../../common/dto";
|
||||
import {
|
||||
CreateCustomerRequestSchema,
|
||||
CustomerListRequestSchema,
|
||||
GetCustomerByIdRequestSchema,
|
||||
} from "../../../common/dto";
|
||||
import { buildCustomerDependencies } from "../dependencies";
|
||||
import { GetCustomerController, ListCustomersController } from "./controllers";
|
||||
import {
|
||||
CreateCustomerController,
|
||||
GetCustomerController,
|
||||
ListCustomersController,
|
||||
} from "./controllers";
|
||||
|
||||
export const customersRouter = (params: ModuleParams) => {
|
||||
const { app, database, baseRoutePath, logger } = params as {
|
||||
@ -61,7 +69,7 @@ export const customersRouter = (params: ModuleParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
/*router.post(
|
||||
router.post(
|
||||
"/",
|
||||
//checkTabContext,
|
||||
|
||||
@ -73,7 +81,7 @@ export const customersRouter = (params: ModuleParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
router.put(
|
||||
/*router.put(
|
||||
"/:customer_id",
|
||||
//checkTabContext,
|
||||
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
||||
|
||||
@ -3,4 +3,5 @@ export * from "./aggregate-root-repository.interface";
|
||||
export * from "./domain-entity";
|
||||
export * from "./events/domain-event.interface";
|
||||
export * from "./helpers";
|
||||
export * from "./specification";
|
||||
export * from "./value-objects";
|
||||
|
||||
132
packages/rdx-ddd/src/specification.ts
Normal file
132
packages/rdx-ddd/src/specification.ts
Normal 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}]`;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user