Catalogos

This commit is contained in:
David Arranz 2026-05-22 20:24:50 +02:00
parent 2a1d49d1c4
commit 567c9d9730
111 changed files with 3295 additions and 22 deletions

View File

@ -7,6 +7,11 @@ import factuGESAPIModule from "@erp/factuges/api";
import { registerModule } from "./lib";
// Aquí hay que registrar los módulos que
// queramos que se carguen en el servidor.
// El registro hace que estén disponibles
// las rutas, los modelos y los servicios
// públicos de ese módulo.
export const registerModules = () => {
//registerModule(authAPIModule);
registerModule(catalogsAPIModule);

View File

@ -1 +1,2 @@
export * from "./payment-methods";
export * from "./payment-terms";

View File

@ -42,7 +42,7 @@ export class CreatePaymentMethodInputMapper implements ICreatePaymentMethodInput
this.throwIfValidationErrors(errors);
const props = {
const props: IPaymentMethodCreateProps = {
companyId: params.companyId,
name: name!,
description: description!,

View File

@ -0,0 +1,7 @@
export * from "./payment-term-creator.di";
export * from "./payment-term-deleter.di";
export * from "./payment-term-finder.di";
export * from "./payment-term-input-mappers.di";
export * from "./payment-term-snapshot-builders.di";
export * from "./payment-term-status-changer.di";
export * from "./payment-term-updater.di";

View File

@ -0,0 +1,10 @@
import type { IPaymentTermRepository } from "../repositories";
import { type IPaymentTermCreator, PaymentTermCreator } from "../services";
export const buildPaymentTermCreator = (params: {
repository: IPaymentTermRepository;
}): IPaymentTermCreator => {
const { repository } = params;
return new PaymentTermCreator(repository);
};

View File

@ -0,0 +1,10 @@
import type { IPaymentTermRepository } from "../repositories";
import { type IPaymentTermDeleter, PaymentTermDeleter } from "../services";
export const buildPaymentTermDeleter = (params: {
repository: IPaymentTermRepository;
}): IPaymentTermDeleter => {
const { repository } = params;
return new PaymentTermDeleter(repository);
};

View File

@ -0,0 +1,10 @@
import type { IPaymentTermRepository } from "../repositories";
import { type IPaymentTermFinder, PaymentTermFinder } from "../services";
export function buildPaymentTermFinder(params: {
repository: IPaymentTermRepository;
}): IPaymentTermFinder {
const { repository } = params;
return new PaymentTermFinder(repository);
}

View File

@ -0,0 +1,20 @@
import {
CreatePaymentTermInputMapper,
type ICreatePaymentTermInputMapper,
UpdatePaymentTermInputMapper,
} from "../mappers";
export interface IPaymentTermInputMappers {
createInputMapper: ICreatePaymentTermInputMapper;
updateInputMapper: UpdatePaymentTermInputMapper;
}
export const buildPaymentTermInputMappers = (): IPaymentTermInputMappers => {
const createInputMapper = new CreatePaymentTermInputMapper();
const updateInputMapper = new UpdatePaymentTermInputMapper();
return {
createInputMapper,
updateInputMapper,
};
};

View File

@ -0,0 +1,14 @@
import {
PaymentTermFullSnapshotBuilder,
PaymentTermSummarySnapshotBuilder,
} from "../snapshot-builders";
export function buildPaymentTermSnapshotBuilders() {
const fullSnapshotBuilder = new PaymentTermFullSnapshotBuilder();
const summarySnapshotBuilder = new PaymentTermSummarySnapshotBuilder();
return {
full: fullSnapshotBuilder,
summary: summarySnapshotBuilder,
};
}

View File

@ -0,0 +1,10 @@
import type { IPaymentTermRepository } from "../repositories";
import { type IPaymentTermStatusChanger, PaymentTermStatusChanger } from "../services";
export const buildPaymentTermStatusChanger = (params: {
repository: IPaymentTermRepository;
}): IPaymentTermStatusChanger => {
const { repository } = params;
return new PaymentTermStatusChanger(repository);
};

View File

@ -0,0 +1,10 @@
import type { IPaymentTermRepository } from "../repositories";
import { type IPaymentTermUpdater, PaymentTermUpdater } from "../services";
export const buildPaymentTermUpdater = (params: {
repository: IPaymentTermRepository;
}): IPaymentTermUpdater => {
const { repository } = params;
return new PaymentTermUpdater(repository);
};

View File

@ -0,0 +1,7 @@
export * from "./di";
export * from "./mappers";
export * from "./models";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases";

View File

@ -0,0 +1,107 @@
import {
DomainError,
Name,
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CreatePaymentTermRequestDTO } from "../../../../common";
import {
type IPaymentTermCreateProps,
PaymentTermDueDays,
PaymentTermPercentage,
} from "../../../domain";
import type { PaymentTermDueRuleCreateProps } from "../../../domain/payment-terms/payment-term-due-rule";
import type { PaymentTermName } from "../../../domain/payment-terms/payment-term-name";
export interface ICreatePaymentTermInputMapper {
map(
dto: CreatePaymentTermRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IPaymentTermCreateProps }, Error>;
}
export class CreatePaymentTermInputMapper implements ICreatePaymentTermInputMapper {
public map(
dto: CreatePaymentTermRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IPaymentTermCreateProps }, Error> {
const errors: ValidationErrorDetail[] = [];
try {
const paymentTermId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const name = extractOrPushError(Name.create(dto.name), "name", errors);
const description = extractOrPushError(
maybeFromNullableResult(dto.description, (value) => TextValue.create(value)),
"description",
errors
);
const isActive = extractOrPushError(Result.ok(dto.is_active), "is_active", errors);
const dueRules = this.mapRulesProps(dto.due_rules, { errors });
this.throwIfValidationErrors(errors);
const props: IPaymentTermCreateProps = {
companyId: params.companyId,
name: name as PaymentTermName,
description: description!,
isActive: isActive!,
isSystem: false,
dueRules,
};
return Result.ok({ id: paymentTermId!, props });
} catch (err: unknown) {
return Result.fail(
new DomainError("Payment term props mapping failed [CreatePaymentTermInputMapper.map]", {
cause: err,
})
);
}
}
private mapRulesProps(
rulesDTO: NonNullable<CreatePaymentTermRequestDTO["due_rules"]>,
params: { errors: ValidationErrorDetail[] }
): PaymentTermDueRuleCreateProps[] {
return rulesDTO.map((item, index) => {
const dueDays = extractOrPushError(
PaymentTermDueDays.create(Number(item.due_days)),
`due_rules[${index}].due_days`,
params.errors
);
const percentage = extractOrPushError(
PaymentTermPercentage.create({
value: Number(item.percentage.value),
scale: Number(item.percentage.scale),
}),
`due_rules[${index}].percentage`,
params.errors
);
return {
dueDays: dueDays!,
percentage: percentage!,
};
});
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection(
"Payment method props mapping failed [CreatePaymentTermInputMapper]",
errors
);
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./create-payment-term-input.mapper";
export * from "./update-payment-term-by-id-input.mapper";

View File

@ -0,0 +1,121 @@
import {
DomainError,
Name,
TextValue,
type UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdatePaymentTermByIdRequestDTO } from "../../../../common";
import {
PaymentTermDueDays,
type PaymentTermDueRulePatchProps,
type PaymentTermPatchProps,
PaymentTermPercentage,
} from "../../../domain";
export interface IUpdatePaymentTermInputMapper {
map(
dto: UpdatePaymentTermByIdRequestDTO,
params: { companyId: UniqueID }
): Result<PaymentTermPatchProps>;
}
/**
* @summary Convierte el DTO de update de payment term en props de dominio.
* @remarks
* Respeta semántica PATCH en cabecera:
* - omitido: no modificar
* - null: limpiar valor cuando el campo lo permite
* - valor: asignar nuevo valor
*
* Para `items`, no aplica patch granular:
* - undefined: no tocar líneas
* - []: borrar todas las líneas
* - [...]: reemplazar colección completa
*/
export class UpdatePaymentTermInputMapper implements IUpdatePaymentTermInputMapper {
public map(
dto: UpdatePaymentTermByIdRequestDTO,
_params: { companyId: UniqueID }
): Result<PaymentTermPatchProps> {
try {
const errors: ValidationErrorDetail[] = [];
const paymentTermPatchProps: PaymentTermPatchProps = {};
toPatchField(dto.name).ifSet((name) => {
if (isNullishOrEmpty(name)) {
errors.push({ path: "name", message: "Name cannot be empty" });
return;
}
paymentTermPatchProps.name = extractOrPushError(Name.create(name), "name", errors);
});
toPatchField(dto.description).ifSet((description) => {
paymentTermPatchProps.description = extractOrPushError(
maybeFromNullableResult(description, (value) => TextValue.create(value)),
"description",
errors
);
});
toPatchField(dto.is_active).ifSet((isActive) => {
paymentTermPatchProps.isActive = isActive;
});
if (dto.due_rules !== undefined) {
paymentTermPatchProps.dueRules = this.mapRulesProps(dto.due_rules, { errors });
}
this.throwIfValidationErrors(errors);
return Result.ok(paymentTermPatchProps);
} catch (err: unknown) {
return Result.fail(new DomainError("Payment term props mapping failed", { cause: err }));
}
}
private mapRulesProps(
rulesDTO: NonNullable<UpdatePaymentTermByIdRequestDTO["due_rules"]>,
params: { errors: ValidationErrorDetail[] }
): PaymentTermDueRulePatchProps[] {
const dueRuleProps = rulesDTO.map((rule, index) => {
const dueDays = extractOrPushError(
PaymentTermDueDays.create(Number(rule.due_days)),
`due_rules[${index}].due_days`,
params.errors
);
const percentage = extractOrPushError(
PaymentTermPercentage.create({
value: Number(rule.percentage.value),
scale: Number(rule.percentage.scale),
}),
`due_rules[${index}].percentage`,
params.errors
);
return {
dueDays: dueDays!,
percentage: percentage!,
};
});
return dueRuleProps;
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection(
"Payment method props mapping failed [CreatePaymentTermInputMapper]",
errors
);
}
}
}

View File

@ -0,0 +1 @@
export * from "./payment-term-summary.model";

View File

@ -0,0 +1,11 @@
import type { PaymentTermDueRule } from "@erp/catalogs/api/domain";
import type { Name, UniqueID } from "@repo/rdx-ddd";
export type PaymentTermSummary = {
id: UniqueID;
companyId: UniqueID;
name: Name;
isActive: boolean;
isSystem: boolean;
dueRules: Array<PaymentTermDueRule>;
};

View File

@ -0,0 +1 @@
export * from './payment-term-repository.interface';

View File

@ -0,0 +1,31 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import type { PaymentTermSummary } from "../models/payment-term-summary.model";
export interface IPaymentTermRepository {
create(paymentTerm: PaymentTerm, transaction?: unknown): Promise<Result<void, Error>>;
update(paymentTerm: PaymentTerm, transaction?: unknown): Promise<Result<void, Error>>;
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<boolean, Error>>;
existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<PaymentTerm, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentTermSummary>, Error>>;
}

View File

@ -0,0 +1,6 @@
export * from './payment-term-creator';
export * from './payment-term-deleter';
export * from './payment-term-finder';
export * from './payment-term-public-services';
export * from './payment-term-status-changer';
export * from './payment-term-updater';

View File

@ -0,0 +1,37 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { type IPaymentTermCreateProps, PaymentTerm } from "../../../domain";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermCreatorParams {
companyId: UniqueID;
id: UniqueID;
props: IPaymentTermCreateProps;
transaction: unknown;
}
export interface IPaymentTermCreator {
create(params: IPaymentTermCreatorParams): Promise<Result<PaymentTerm, Error>>;
}
export class PaymentTermCreator implements IPaymentTermCreator {
constructor(private readonly repository: IPaymentTermRepository) {}
public async create(params: IPaymentTermCreatorParams): Promise<Result<PaymentTerm, Error>> {
const { companyId, id, props, transaction } = params;
const paymentTermResult = PaymentTerm.create({ ...props, companyId }, id);
if (paymentTermResult.isFailure) {
return Result.fail(paymentTermResult.error);
}
const paymentTerm = paymentTermResult.data;
const saveResult = await this.repository.create(paymentTerm, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -0,0 +1,41 @@
import { Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import { PaymentTermCannotBeDeletedError } from "../../../domain";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermDeleter {
delete(params: {
paymentTerm: PaymentTerm;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>>;
}
export class PaymentTermDeleter implements IPaymentTermDeleter {
public constructor(private readonly repository: IPaymentTermRepository) {}
public async delete(params: {
paymentTerm: PaymentTerm;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>> {
const { paymentTerm, transaction } = params;
if (paymentTerm.isSystem) {
return Result.fail(
new PaymentTermCannotBeDeletedError("System payment terms cannot be deleted.")
);
}
const deleteResult = await this.repository.deleteByIdInCompany(
paymentTerm.companyId,
paymentTerm.id,
transaction
);
if (deleteResult.isFailure) {
return Result.fail(deleteResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -0,0 +1,55 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import type { PaymentTermSummary } from "../models/payment-term-summary.model";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermFinder {
findPaymentTermById(
companyId: UniqueID,
paymentTermId: UniqueID,
transaction?: unknown
): Promise<Result<PaymentTerm, Error>>;
paymentTermExists(
companyId: UniqueID,
paymentTermId: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
findPaymentTermsByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentTermSummary>, Error>>;
}
export class PaymentTermFinder implements IPaymentTermFinder {
constructor(private readonly repository: IPaymentTermRepository) {}
public async findPaymentTermById(
companyId: UniqueID,
paymentTermId: UniqueID,
transaction?: unknown
): Promise<Result<PaymentTerm, Error>> {
return this.repository.getByIdInCompany(companyId, paymentTermId, transaction);
}
public async paymentTermExists(
companyId: UniqueID,
paymentTermId: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, paymentTermId, transaction);
}
public async findPaymentTermsByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentTermSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
}

View File

@ -0,0 +1,11 @@
import type { IPaymentTermFinder } from "./payment-term-finder";
export interface IPaymentTermPublicServices {
finder: IPaymentTermFinder;
}
export const buildPaymentTermPublicServices = (
finder: IPaymentTermFinder
): IPaymentTermPublicServices => ({
finder,
});

View File

@ -0,0 +1,42 @@
import { Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export type PaymentTermStatusChangeAction = "enable" | "disable";
export interface IPaymentTermStatusChanger {
changeStatus(params: {
paymentTerm: PaymentTerm;
action: PaymentTermStatusChangeAction;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>>;
}
export class PaymentTermStatusChanger implements IPaymentTermStatusChanger {
public constructor(private readonly repository: IPaymentTermRepository) {}
public async changeStatus(params: {
paymentTerm: PaymentTerm;
action: PaymentTermStatusChangeAction;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>> {
const { paymentTerm, action, transaction } = params;
const statusResult = action === "enable" ? paymentTerm.enable() : paymentTerm.disable();
if (statusResult.isFailure) {
return Result.fail(statusResult.error);
}
if (!statusResult.data) {
return Result.ok(paymentTerm);
}
const persistenceResult = await this.repository.update(paymentTerm, transaction);
if (persistenceResult.isFailure) {
return Result.fail(persistenceResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -0,0 +1,49 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentTerm, PaymentTermPatchProps } from "../../../domain";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermUpdater {
update(params: {
companyId: UniqueID;
id: UniqueID;
patchProps: PaymentTermPatchProps;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>>;
}
export class PaymentTermUpdater implements IPaymentTermUpdater {
constructor(private readonly repository: IPaymentTermRepository) {}
public async update(params: {
companyId: UniqueID;
id: UniqueID;
patchProps: PaymentTermPatchProps;
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>> {
const { companyId, id, patchProps, transaction } = params;
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const paymentTerm = existingResult.data;
const updateResult = paymentTerm.update(patchProps);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
if (!updateResult.data) {
return Result.ok(paymentTerm);
}
const saveResult = await this.repository.update(paymentTerm, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -0,0 +1 @@
export * from "./payment-term-full-snapshot-builder";

View File

@ -0,0 +1,25 @@
import type { GetPaymentTermByIdResponseDTO } from "@erp/catalogs/common";
import type { ISnapshotBuilder } from "@erp/core/api";
import { toNullable } from "@repo/rdx-ddd";
import type { PaymentTerm } from "../../../../domain/payment-terms";
export interface IPaymentTermFullSnapshotBuilder
extends ISnapshotBuilder<PaymentTerm, GetPaymentTermByIdResponseDTO> {}
export class PaymentTermFullSnapshotBuilder implements IPaymentTermFullSnapshotBuilder {
public toOutput(paymentTerm: PaymentTerm): GetPaymentTermByIdResponseDTO {
return {
id: paymentTerm.id.toPrimitive(),
company_id: paymentTerm.companyId.toPrimitive(),
name: paymentTerm.name.toPrimitive(),
description: toNullable(paymentTerm.description, (value) => value.toPrimitive()),
is_active: paymentTerm.isActive,
is_system: paymentTerm.isSystem,
due_rules: paymentTerm.dueRules.map((rule) => ({
due_days: rule.dueDays.toString(),
percentage: rule.percentage.toObjectString(),
})),
};
}
}

View File

@ -0,0 +1,2 @@
export * from "./full";
export * from "./summary";

View File

@ -0,0 +1,15 @@
import type { PaymentTermDueRuleDTO } from "../../../../common";
import type { PaymentTermDueRule } from "../../../domain/payment-terms/payment-term-due-rule";
export interface IPaymentTermDueRuleSnapshotBuilder {
toOutput(paymentTermDueRule: PaymentTermDueRule): PaymentTermDueRuleDTO;
}
export class PaymentTermDueRuleSnapshotBuilder implements IPaymentTermDueRuleSnapshotBuilder {
public toOutput(paymentTermDueRule: PaymentTermDueRule): PaymentTermDueRuleDTO {
return {
due_days: paymentTermDueRule.dueDays.toString(),
percentage: paymentTermDueRule.percentage.toObjectString(),
};
}
}

View File

@ -0,0 +1 @@
export * from "./payment-term-summary-snapshot-builder";

View File

@ -0,0 +1,23 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import type { PaymentTermSummaryDTO } from "../../../../../common";
import type { PaymentTermSummary } from "../../models";
export interface IPaymentTermSummarySnapshotBuilder
extends ISnapshotBuilder<PaymentTermSummary, PaymentTermSummaryDTO> {}
export class PaymentTermSummarySnapshotBuilder implements IPaymentTermSummarySnapshotBuilder {
public toOutput(paymentTerm: PaymentTermSummary): PaymentTermSummaryDTO {
return {
id: paymentTerm.id.toString(),
company_id: paymentTerm.companyId.toString(),
name: paymentTerm.name.toString(),
is_system: paymentTerm.isSystem,
is_active: paymentTerm.isActive,
due_rules: paymentTerm.dueRules.map((rule) => ({
due_days: rule.dueDays.toString(),
percentage: rule.percentage.toObjectString(),
})),
};
}
}

View File

@ -0,0 +1,55 @@
import type { CreatePaymentTermRequestDTO } from "@erp/catalogs/common";
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ICreatePaymentTermInputMapper } from "../mappers";
import type { IPaymentTermCreator } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type CreatePaymentTermUseCaseInput = {
companyId: UniqueID;
dto: CreatePaymentTermRequestDTO;
};
type CreatePaymentTermUseCaseDeps = {
dtoMapper: ICreatePaymentTermInputMapper;
creator: IPaymentTermCreator;
fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class CreatePaymentTermUseCase {
constructor(private readonly deps: CreatePaymentTermUseCaseDeps) {}
public execute(params: CreatePaymentTermUseCaseInput) {
const { dto, companyId } = params;
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) {
return mappedPropsResult;
}
const { props, id } = mappedPropsResult.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const createResult = await this.deps.creator.create({
companyId,
id,
props,
transaction,
});
if (createResult.isFailure) {
return createResult;
}
const snapshot = this.deps.fullSnapshotBuilder.toOutput(createResult.data);
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,57 @@
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentTermDeleter, IPaymentTermFinder } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type DeletePaymentTermByIdUseCaseInput = {
companyId: UniqueID;
payment_term_id: string;
};
export class DeletePaymentTermByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentTermFinder;
deleter: IPaymentTermDeleter;
fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DeletePaymentTermByIdUseCaseInput) {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
const paymentTermId = idOrError.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const findResult = await this.deps.finder.findPaymentTermById(
companyId,
paymentTermId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const deleteResult = await this.deps.deleter.delete({
paymentTerm: findResult.data,
transaction,
});
if (deleteResult.isFailure) {
return Result.fail(deleteResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(deleteResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,58 @@
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentTermFinder, IPaymentTermStatusChanger } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type DisablePaymentTermByIdUseCaseInput = {
companyId: UniqueID;
payment_term_id: string;
};
export class DisablePaymentTermByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentTermFinder;
changer: IPaymentTermStatusChanger;
fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DisablePaymentTermByIdUseCaseInput) {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
const paymentTermId = idOrError.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const findResult = await this.deps.finder.findPaymentTermById(
companyId,
paymentTermId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const disableResult = await this.deps.changer.changeStatus({
paymentTerm: findResult.data,
action: "disable",
transaction,
});
if (disableResult.isFailure) {
return Result.fail(disableResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(disableResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,58 @@
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentTermFinder, IPaymentTermStatusChanger } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type EnablePaymentTermByIdUseCaseInput = {
companyId: UniqueID;
payment_term_id: string;
};
export class EnablePaymentTermByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentTermFinder;
changer: IPaymentTermStatusChanger;
fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: EnablePaymentTermByIdUseCaseInput) {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
const paymentTermId = idOrError.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const findResult = await this.deps.finder.findPaymentTermById(
companyId,
paymentTermId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const enableResult = await this.deps.changer.changeStatus({
paymentTerm: findResult.data,
action: "enable",
transaction,
});
if (enableResult.isFailure) {
return Result.fail(enableResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(enableResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,44 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentTermFinder } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type GetPaymentTermByIdUseCaseInput = {
companyId: UniqueID;
payment_term_id: string;
};
export class GetPaymentTermByIdUseCase {
constructor(
private readonly finder: IPaymentTermFinder,
private readonly fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: GetPaymentTermByIdUseCaseInput) {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentTermId = idOrError.data;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const result = await this.finder.findPaymentTermById(companyId, paymentTermId, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(this.fullSnapshotBuilder.toOutput(result.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,7 @@
export * from "./create-payment-term.use-case";
export * from "./delete-payment-term-by-id.use-case";
export * from "./disable-payment-term-by-id.use-case";
export * from "./enable-payment-term-by-id.use-case";
export * from "./get-payment-term-by-id.use-case";
export * from "./list-payment-terms.use-case";
export * from "./update-payment-term-by-id.use-case";

View File

@ -0,0 +1,59 @@
import type { ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentTermFinder } from "../services";
import type { IPaymentTermSummarySnapshotBuilder } from "../snapshot-builders";
type ListPaymentTermsUseCaseInput = {
companyId: UniqueID;
criteria: Criteria;
};
export class ListPaymentTermsUseCase {
constructor(
private readonly finder: IPaymentTermFinder,
private readonly summarySnapshotBuilder: IPaymentTermSummarySnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: ListPaymentTermsUseCaseInput) {
const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const result = await this.finder.findPaymentTermsByCriteria(
companyId,
criteria,
transaction
);
if (result.isFailure) {
return Result.fail(result.error);
}
const paymentTerms = result.data;
const totalItems = paymentTerms.total();
const items = paymentTerms.map((item) => this.summarySnapshotBuilder.toOutput(item));
const snapshot = {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems,
items,
metadata: {
entity: "payment_terms",
criteria: criteria.toJSON(),
},
};
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,63 @@
import type { UpdatePaymentTermByIdRequestDTO } from "@erp/catalogs/common";
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IUpdatePaymentTermInputMapper } from "../mappers";
import type { IPaymentTermFinder, IPaymentTermUpdater } from "../services";
import type { IPaymentTermFullSnapshotBuilder } from "../snapshot-builders";
export type UpdatePaymentTermByIdUseCaseInput = {
companyId: UniqueID;
payment_term_id: string;
dto: UpdatePaymentTermByIdRequestDTO;
};
export class UpdatePaymentTermByIdUseCase {
constructor(
private readonly deps: {
updater: IPaymentTermUpdater;
finder: IPaymentTermFinder;
dtoMapper: IUpdatePaymentTermInputMapper;
fullSnapshotBuilder: IPaymentTermFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: UpdatePaymentTermByIdUseCaseInput) {
const { companyId, payment_term_id, dto } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentTermId = idOrError.data;
const patchPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (patchPropsResult.isFailure) {
return patchPropsResult;
}
const patchProps = patchPropsResult.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const updateResult = await this.deps.updater.update({
companyId,
id: paymentTermId,
patchProps,
transaction,
});
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(updateResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1 +1,2 @@
export * from "./payment-methods";
export * from "./payment-terms";

View File

@ -0,0 +1,126 @@
import { DomainError } from "@repo/rdx-ddd";
export class InvalidPaymentTermIdError extends DomainError {
public readonly code = "PAYMENT_TERM_INVALID_ID" as const;
}
export const isInvalidPaymentTermIdError = (e: unknown): e is InvalidPaymentTermIdError =>
e instanceof InvalidPaymentTermIdError;
export class InvalidPaymentTermNameError extends DomainError {
public readonly code = "PAYMENT_TERM_NAME" as const;
}
export const isInvalidPaymentTermNameError = (e: unknown): e is InvalidPaymentTermNameError =>
e instanceof InvalidPaymentTermNameError;
export class InvalidPaymentTermDescriptionError extends DomainError {
public readonly code = "PAYMENT_TERM_DESCRIPTION" as const;
}
export const isInvalidPaymentTermDescriptionError = (
e: unknown
): e is InvalidPaymentTermDescriptionError => e instanceof InvalidPaymentTermDescriptionError;
export class InvalidPaymentTermDueDaysError extends DomainError {
public readonly code = "PAYMENT_TERM_DUE_DAYS" as const;
}
export const isInvalidPaymentTermDueDaysError = (e: unknown): e is InvalidPaymentTermDueDaysError =>
e instanceof InvalidPaymentTermDueDaysError;
export class InvalidPaymentTermPercentageError extends DomainError {
public readonly code = "PAYMENT_TERM_PERCENTAGE" as const;
}
export const isInvalidPaymentTermPercentageError = (
e: unknown
): e is InvalidPaymentTermPercentageError => e instanceof InvalidPaymentTermPercentageError;
export class InvalidPaymentTermDueRulesError extends DomainError {
public readonly code = "PAYMENT_TERM_DUE_RULES" as const;
}
export const isInvalidPaymentTermDueRulesError = (
e: unknown
): e is InvalidPaymentTermDueRulesError => e instanceof InvalidPaymentTermDueRulesError;
export class PaymentTermDueDaysDuplicatedError extends DomainError {
public readonly code = "PAYMENT_TERM_DUE_DAYS_DUPLICATED" as const;
}
export const isPaymentTermDueDaysDuplicatedError = (
e: unknown
): e is PaymentTermDueDaysDuplicatedError => e instanceof PaymentTermDueDaysDuplicatedError;
export class PaymentTermPercentageSumMismatchError extends DomainError {
public readonly code = "PAYMENT_TERM_PERCENTAGE_SUM_MISMATCH" as const;
}
export const isPaymentTermPercentageSumMismatchError = (
e: unknown
): e is PaymentTermPercentageSumMismatchError => e instanceof PaymentTermPercentageSumMismatchError;
export class PaymentTermNotFoundError extends DomainError {
public readonly code = "PAYMENT_TERM_NOT_FOUND" as const;
}
export const isPaymentTermNotFoundError = (e: unknown): e is PaymentTermNotFoundError =>
e instanceof PaymentTermNotFoundError;
export class PaymentTermCannotBeUpdatedError extends DomainError {
public readonly code = "PAYMENT_TERM_CANNOT_BE_UPDATED" as const;
}
export const isPaymentTermCannotBeUpdatedError = (
e: unknown
): e is PaymentTermCannotBeUpdatedError => e instanceof PaymentTermCannotBeUpdatedError;
export class PaymentTermCannotBeDeletedError extends DomainError {
public readonly code = "PAYMENT_TERM_CANNOT_BE_DELETED" as const;
}
export const isPaymentTermCannotBeDeletedError = (
e: unknown
): e is PaymentTermCannotBeDeletedError => e instanceof PaymentTermCannotBeDeletedError;
export class PaymentTermCannotBeEnabledError extends DomainError {
public readonly code = "PAYMENT_TERM_CANNOT_BE_ENABLED" as const;
}
export const isPaymentTermCannotBeEnabledError = (
e: unknown
): e is PaymentTermCannotBeEnabledError => e instanceof PaymentTermCannotBeEnabledError;
export class PaymentTermCannotBeDisabledError extends DomainError {
public readonly code = "PAYMENT_TERM_CANNOT_BE_DISABLED" as const;
}
export const isPaymentTermCannotBeDisabledError = (
e: unknown
): e is PaymentTermCannotBeDisabledError => e instanceof PaymentTermCannotBeDisabledError;
export class PaymentTermDueRuleMismatch extends DomainError {
/**
* Crea una instancia del error con el identificador del item.
*
* @param position - Posición del item
* @param options - Opciones nativas de Error (puedes pasar `cause`).
*/
constructor(position: number, options?: ErrorOptions) {
super(
`Error. Payment term due rule with position '${position}' rejected due to currency/language mismatch.`,
options
);
this.name = "PaymentTermDueRuleMismatch";
}
}
/**
* *Type guard* para `PaymentTermDueRuleMismatch`.
*
* @param e - Error desconocido
* @returns `true` si `e` es `PaymentTermDueRuleMismatch`
*/
export const isPaymentTermDueRuleMismatch = (e: unknown): e is PaymentTermDueRuleMismatch =>
e instanceof PaymentTermDueRuleMismatch;

View File

@ -0,0 +1,7 @@
export * from "./errors";
export * from "./payment-term.aggregate";
export * from "./payment-term-due-days";
export * from "./payment-term-due-rule";
export * from "./payment-term-due-rules.collection";
export * from "./payment-term-name";
export * from "./payment-term-percentage";

View File

@ -0,0 +1,38 @@
import { ValueObject, translateZodValidationError } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
export class PaymentTermDueDays extends ValueObject<{ value: number }> {
protected constructor(props: { value: number }) {
super(props);
}
protected static validate(value: number) {
const schema = z.number().int().min(0, "Payment term due days must be a non-negative integer");
return schema.safeParse(value);
}
public static create(value: number): Result<PaymentTermDueDays, Error> {
const validation = PaymentTermDueDays.validate(value);
if (!validation.success) {
return Result.fail(
translateZodValidationError("InvalidPaymentTermDueDays", validation.error)
);
}
return Result.ok(new PaymentTermDueDays({ value: validation.data }));
}
public get value(): number {
return this.props.value;
}
public toPrimitive(): number {
return this.props.value;
}
public getProps() {
return this.props;
}
}

View File

@ -0,0 +1,88 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentTermDueDays } from "./payment-term-due-days";
import type { PaymentTermPercentage } from "./payment-term-percentage";
export type PaymentTermDueRuleCreateProps = {
dueDays: PaymentTermDueDays;
percentage: PaymentTermPercentage;
};
export type PaymentTermDueRulePatchProps = PaymentTermDueRuleCreateProps;
type InternalPaymentTermDueRuleProps = PaymentTermDueRuleCreateProps;
export class PaymentTermDueRule extends DomainEntity<InternalPaymentTermDueRuleProps> {
static create(
props: PaymentTermDueRuleCreateProps,
id?: UniqueID
): Result<PaymentTermDueRule, Error> {
if (!props.dueDays) {
return Result.fail(new Error("Payment term due days is required"));
}
if (!props.percentage) {
return Result.fail(new Error("Payment term percentage is required"));
}
const newPaymentTermDueRule = new PaymentTermDueRule(props, id);
return Result.ok(newPaymentTermDueRule);
}
static rehydrate(props: InternalPaymentTermDueRuleProps, id: UniqueID): PaymentTermDueRule {
return new PaymentTermDueRule(props, id);
}
protected constructor(props: InternalPaymentTermDueRuleProps, id?: UniqueID) {
super(props, id);
}
/*public static fromDTO(dto: PaymentTermDueRuleDTO): Result<PaymentTermDueRule, Error> {
const errors: Array<{ path: string; message: string }> = [];
const dueDaysResult = PaymentTermDueDays.create(dto.due_days);
const percentageResult = PaymentTermPercentage.create(Number(dto.percentage.value));
if (dueDaysResult.isFailure) {
errors.push({ path: "due_days", message: dueDaysResult.error.message });
}
if (percentageResult.isFailure) {
errors.push({ path: "percentage", message: percentageResult.error.message });
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("InvalidPaymentTermDueRule", errors));
}
return PaymentTermDueRule.create({
dueDays: dueDaysResult.data,
percentage: percentageResult.data,
});
}*/
public get dueDays(): PaymentTermDueDays {
return this.props.dueDays;
}
public get percentage(): PaymentTermPercentage {
return this.props.percentage;
}
public toPrimitive() {
return {
due_days: this.dueDays.toPrimitive(),
percentage: this.percentage.toPrimitive(),
};
}
public equals(other: PaymentTermDueRule): boolean {
return (
this.dueDays.toPrimitive() === other.dueDays.toPrimitive() &&
this.percentage.value === other.percentage.value &&
this.percentage.scale === other.percentage.scale
);
}
}

View File

@ -0,0 +1,67 @@
import { Collection } from "@repo/rdx-utils";
import {
InvalidPaymentTermPercentageError,
PaymentTermDueDaysDuplicatedError,
PaymentTermPercentageSumMismatchError,
} from "./errors";
import type { PaymentTermDueRule } from "./payment-term-due-rule";
export interface PaymentTermDueRulesProps {
rules?: PaymentTermDueRule[];
}
// OJO, no extendemos de Collection<IProformaItem> para no exponer
// públicamente métodos para manipular la colección.
export type IPaymentTermDueRules = {};
export class PaymentTermDueRules
extends Collection<PaymentTermDueRule>
implements IPaymentTermDueRules
{
// OJO, no extendemos de Collection<PaymentTermDueRule> para no exponer
// públicamente métodos para manipular la colección.
private constructor(props: PaymentTermDueRulesProps) {
super(props.rules ?? []);
}
public static create(props: PaymentTermDueRulesProps): PaymentTermDueRules {
const { rules } = props;
if (!rules || rules.length === 0) {
return new PaymentTermDueRules({ rules: [] });
}
const ordered = [...rules].sort(
(first, second) => first.dueDays.toPrimitive() - second.dueDays.toPrimitive()
);
const seenDueDays = new Set<number>();
let totalPercentage = 0;
for (const item of ordered) {
const dueDays = item.dueDays.toPrimitive();
if (seenDueDays.has(dueDays)) {
throw new PaymentTermDueDaysDuplicatedError(
`Duplicate dueDays found in payment term due rules: ${dueDays}`
);
}
seenDueDays.add(dueDays);
const percentage = item.percentage;
if (percentage.scale !== 2) {
throw new InvalidPaymentTermPercentageError("Payment term percentage scale must be 2.");
}
totalPercentage += percentage.value;
}
if (totalPercentage !== 10000) {
throw new PaymentTermPercentageSumMismatchError(
"Payment term due rule percentages must sum exactly 100.00."
);
}
return new PaymentTermDueRules({ rules: ordered });
}
}

View File

@ -0,0 +1,4 @@
import { Name } from "@repo/rdx-ddd";
export const PaymentTermName = Name;
export type PaymentTermName = Name;

View File

@ -0,0 +1,29 @@
import { Percentage, type PercentageProps, ValidationErrorCollection } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type PaymentTermPercentageProps = PercentageProps;
export class PaymentTermPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value, scale }: PaymentTermPercentageProps): Result<Percentage> {
if (scale && scale !== PaymentTermPercentage.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `PaymentTermPercentage scale must be ${PaymentTermPercentage.DEFAULT_SCALE}` },
])
);
}
return Result.ok(
new PaymentTermPercentage({
value,
scale: PaymentTermPercentage.DEFAULT_SCALE,
})
);
}
static zero() {
return PaymentTermPercentage.create({ value: 0 }).data;
}
}

View File

@ -0,0 +1,229 @@
import { AggregateRoot, type TextValue, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import {
PaymentTermCannotBeDisabledError,
PaymentTermCannotBeUpdatedError,
PaymentTermDueRuleMismatch,
} from "./errors";
import {
PaymentTermDueRule,
type PaymentTermDueRuleCreateProps,
type PaymentTermDueRulePatchProps,
} from "./payment-term-due-rule";
import { PaymentTermDueRules } from "./payment-term-due-rules.collection";
import type { PaymentTermName } from "./payment-term-name";
export interface IPaymentTermCreateProps {
companyId: UniqueID;
name: PaymentTermName;
description: Maybe<TextValue>;
isActive: boolean;
isSystem: boolean;
dueRules: PaymentTermDueRuleCreateProps[];
}
export type PaymentTermPatchProps = Partial<Omit<IPaymentTermCreateProps, "dueRules">> & {
dueRules?: PaymentTermDueRulePatchProps[];
};
export type PaymentTermInternalProps = Omit<IPaymentTermCreateProps, "dueRules">;
export class PaymentTerm extends AggregateRoot<PaymentTermInternalProps> {
private _dueRules: PaymentTermDueRules;
protected constructor(
props: PaymentTermInternalProps,
dueRules: PaymentTermDueRules,
id?: UniqueID
) {
super(props, id);
this._dueRules = dueRules;
}
public static create(props: IPaymentTermCreateProps, id?: UniqueID): Result<PaymentTerm, Error> {
const validationResult = PaymentTerm.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const internalDueRules = PaymentTermDueRules.create({
rules: [],
});
const { dueRules, ...internalProps } = props;
const paymentTerm = new PaymentTerm(internalProps, internalDueRules, id);
const initializeResult = paymentTerm.initializeRules(dueRules);
if (initializeResult.isFailure) {
return Result.fail(initializeResult.error);
}
return Result.ok(paymentTerm);
}
public static rehydrate(
props: PaymentTermInternalProps,
rules: PaymentTermDueRules,
id: UniqueID
): PaymentTerm {
return new PaymentTerm(props, rules, id);
}
private static validateCreateProps(props: IPaymentTermCreateProps): Result<void, Error> {
if (!props.companyId) {
return Result.fail(new Error("Payment term company ID is required"));
}
if (!props.name) {
return Result.fail(new Error("Payment term name is required"));
}
if (!props.dueRules || props.dueRules.length === 0) {
return Result.fail(new Error("Payment term due rules are required"));
}
return Result.ok();
}
private static validatePatchProps(patchProps: PaymentTermPatchProps): Result<void, Error> {
if (Object.keys(patchProps).length === 0) {
return Result.ok();
}
return Result.ok();
}
private initializeRules(
rulesProps: PaymentTermDueRuleCreateProps[] | PaymentTermDueRulePatchProps[]
): Result<void, Error> {
this._dueRules.reset();
for (const [index, ruleProps] of rulesProps.entries()) {
const ruleResult = PaymentTermDueRule.create(ruleProps);
if (ruleResult.isFailure) {
return Result.fail(ruleResult.error);
}
const added = this._dueRules.add(ruleResult.data);
if (!added) {
return Result.fail(new PaymentTermDueRuleMismatch(index));
}
}
return Result.ok();
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get name(): PaymentTermName {
return this.props.name;
}
public get description(): Maybe<TextValue> {
return this.props.description;
}
public get isActive(): boolean {
return this.props.isActive;
}
public get isSystem(): boolean {
return this.props.isSystem;
}
public get dueRules(): PaymentTermDueRules {
return this._dueRules;
}
public update(patchProps: PaymentTermPatchProps): Result<boolean, Error> {
if (this.isSystem) {
return Result.fail(
new PaymentTermCannotBeUpdatedError("System payment terms cannot be updated.")
);
}
const validationResult = PaymentTerm.validatePatchProps(patchProps);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
let hasChanges = false;
if (
patchProps.name !== undefined &&
patchProps.name.toPrimitive() !== this.props.name.toPrimitive()
) {
this.props.name = patchProps.name;
hasChanges = true;
}
if (
patchProps.description !== undefined &&
!PaymentTerm.sameDescription(this.props.description, patchProps.description)
) {
this.props.description = patchProps.description;
hasChanges = true;
}
if (patchProps.isActive !== undefined && this.props.isActive !== patchProps.isActive) {
this.props.isActive = patchProps.isActive;
hasChanges = true;
}
// Reemplazo de items (si se proporciona)
if (patchProps.dueRules !== undefined) {
const initializeResult = this.initializeRules(patchProps.dueRules);
if (initializeResult.isFailure) {
return Result.fail(initializeResult.error);
}
hasChanges = true;
}
return Result.ok(hasChanges);
}
public disable(): Result<boolean, Error> {
if (this.isSystem) {
return Result.fail(
new PaymentTermCannotBeDisabledError("System payment terms cannot be disabled.")
);
}
if (!this.isActive) {
return Result.ok(false);
}
this.props.isActive = false;
return Result.ok(true);
}
public enable(): Result<boolean, Error> {
if (this.isSystem) {
return Result.ok(false);
}
if (this.isActive) {
return Result.ok(false);
}
this.props.isActive = true;
return Result.ok(true);
}
private static sameDescription(current: Maybe<TextValue>, next: Maybe<TextValue>): boolean {
return current.match(
(currentValue: TextValue) =>
next.match(
(nextValue: TextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(),
() => false
),
() => next.isNone()
);
}
}

View File

@ -1,10 +1,15 @@
import type { IModuleServer } from "@erp/core/api";
import { models, paymentMethodsRouter } from "./infrastructure";
import {
paymentMethodModels,
paymentMethodsRouter,
paymentTermModels,
paymentTermsRouter,
} from "./infrastructure";
import {
buildCatalogsDependencies,
buildCatalogsPublicServices,
} from "./infrastructure/payment-methods/di/catalogs.di";
} from "./infrastructure/di/catalogs.di";
export * from "./infrastructure/payment-methods/persistence/sequelize";
@ -22,9 +27,10 @@ export const catalogsAPIModule: IModuleServer = {
});
return {
models,
models: [...paymentMethodModels, ...paymentTermModels],
services: {
paymentMethod: publicServices,
paymentMethod: publicServices.paymentMethods,
paymentTerms: publicServices.paymentTerms,
},
internal,
};
@ -34,6 +40,7 @@ export const catalogsAPIModule: IModuleServer = {
const { logger } = params;
paymentMethodsRouter(params);
paymentTermsRouter(params);
logger.info("🚀 Catalogs module started", {
label: this.name,

View File

@ -4,20 +4,28 @@ import {
type PaymentMethodsInternalDeps,
buildPaymentMethodsDependencies,
buildPaymentMethodsPublicServices,
} from "./payment-methods.di";
} from "../payment-methods/di";
import {
type PaymentTermsInternalDeps,
buildPaymentTermsDependencies,
buildPaymentTermsPublicServices,
} from "../payment-terms/di";
export type CatalogsInternalDeps = {
paymentMethods: PaymentMethodsInternalDeps;
paymentTerms: PaymentTermsInternalDeps;
};
export const buildCatalogsDependencies = (params: ModuleParams): CatalogsInternalDeps => {
return {
paymentMethods: buildPaymentMethodsDependencies(params),
paymentTerms: buildPaymentTermsDependencies(params),
};
};
export const buildCatalogsPublicServices = (params: SetupParams, deps: CatalogsInternalDeps) => {
return {
paymentMethods: buildPaymentMethodsPublicServices(params, deps.paymentMethods),
paymentTerms: buildPaymentTermsPublicServices(params, deps.paymentTerms),
};
};

View File

@ -0,0 +1 @@
export * from "./catalogs.di.ts";

View File

@ -1 +1,2 @@
export * from "./payment-methods";
export * from "./payment-terms";

View File

@ -1 +1,3 @@
export * from "./catalogs.di";
export * from "./payment-method-persistence-mappers.di";
export * from "./payment-method-repositories.di";
export * from "./payment-methods.di";

View File

@ -5,8 +5,8 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { CreatePaymentMethodRequestDTO } from "../../../../../../common";
import type { CreatePaymentMethodUseCase } from "../../../../../application";
import type { CreatePaymentMethodRequestDTO } from "../../../../../common";
import type { CreatePaymentMethodUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class CreatePaymentMethodController extends ExpressController {

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeletePaymentMethodByIdUseCase } from "../../../../../application";
import type { DeletePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class DeletePaymentMethodByIdController extends ExpressController {

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DisablePaymentMethodByIdUseCase } from "../../../../../application";
import type { DisablePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class DisablePaymentMethodByIdController extends ExpressController {

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { EnablePaymentMethodByIdUseCase } from "../../../../../application";
import type { EnablePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class EnablePaymentMethodByIdController extends ExpressController {

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { GetPaymentMethodByIdUseCase } from "../../../../../application";
import type { GetPaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class GetPaymentMethodByIdController extends ExpressController {

View File

@ -6,7 +6,7 @@ import {
} from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListPaymentMethodsUseCase } from "../../../../../application";
import type { ListPaymentMethodsUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class ListPaymentMethodsController extends ExpressController {

View File

@ -6,7 +6,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { UpdatePaymentMethodByIdUseCase } from "../../../../../application";
import type { UpdatePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class UpdatePaymentMethodByIdController extends ExpressController {

View File

@ -1 +1 @@
export * from "./payment-methods/payment-methods.routes";
export * from "./payment-methods.routes";

View File

@ -21,7 +21,7 @@ import {
isPaymentMethodCannotBeEnabledError,
isPaymentMethodCannotBeUpdatedError,
isPaymentMethodNotFoundError,
} from "../../../../domain/payment-methods";
} from "../../../domain/payment-methods";
const invalidPaymentMethodIdRule: ErrorToApiRule = {
priority: 120,

View File

@ -9,7 +9,7 @@ import {
ListPaymentMethodsRequestSchema,
UpdatePaymentMethodByIdParamsRequestSchema,
UpdatePaymentMethodByIdRequestSchema,
} from "../../../../../common";
} from "../../../../common";
import type { CatalogsInternalDeps } from "../../di/catalogs.di";
import {

View File

@ -4,4 +4,4 @@ export * from "./repositories";
import paymentMethodModelInit from "./models/sequelize-payment-method.model";
export const models = [paymentMethodModelInit];
export const paymentMethodModels = [paymentMethodModelInit];

View File

@ -71,14 +71,12 @@ export class SequelizePaymentMethodRepository
}
const { id, ...payload } = dtoResult.data;
const [affected, updated] = await PaymentMethodModel.update(payload, {
const [affected] = await PaymentMethodModel.update(payload, {
where: { id },
transaction,
individualHooks: true,
});
console.log("Update result:", { affected, updated });
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or payment method not found")

View File

@ -0,0 +1,3 @@
export * from "./payment-term-persistence-mappers.di";
export * from "./payment-term-repositories.di";
export * from "./payment-terms.di";

View File

@ -0,0 +1,19 @@
import {
SequelizePaymentTermDomainMapper,
SequelizePaymentTermSummaryMapper,
} from "../persistence";
export interface IPaymentTermPersistenceMappers {
domainMapper: SequelizePaymentTermDomainMapper;
listMapper: SequelizePaymentTermSummaryMapper;
}
export const buildPaymentTermPersistenceMappers = (): IPaymentTermPersistenceMappers => {
const domainMapper = new SequelizePaymentTermDomainMapper();
const listMapper = new SequelizePaymentTermSummaryMapper();
return {
domainMapper,
listMapper,
};
};

View File

@ -0,0 +1,13 @@
import type { Sequelize } from "sequelize";
import { SequelizePaymentTermRepository } from "../persistence";
import type { IPaymentTermPersistenceMappers } from "./payment-term-persistence-mappers.di";
export const buildPaymentTermRepository = (params: {
database: Sequelize;
mappers: IPaymentTermPersistenceMappers;
}) => {
const { database, mappers } = params;
return new SequelizePaymentTermRepository(mappers.domainMapper, mappers.listMapper, database);
};

View File

@ -0,0 +1,117 @@
import type { ModuleParams, SetupParams } from "@erp/core/api";
import { buildTransactionManager } from "@erp/core/api";
import type { Sequelize } from "sequelize";
import {
CreatePaymentTermUseCase,
DeletePaymentTermByIdUseCase,
DisablePaymentTermByIdUseCase,
EnablePaymentTermByIdUseCase,
GetPaymentTermByIdUseCase,
ListPaymentTermsUseCase,
UpdatePaymentTermByIdUseCase,
buildPaymentTermCreator,
buildPaymentTermDeleter,
buildPaymentTermFinder,
buildPaymentTermInputMappers,
buildPaymentTermSnapshotBuilders,
buildPaymentTermStatusChanger,
buildPaymentTermUpdater,
} from "../../../application/payment-terms";
import type { IPaymentTermRepository } from "../../../application/payment-terms/repositories";
import { PaymentTermFinder } from "../../../application/payment-terms/services";
import { buildPaymentTermPersistenceMappers } from "./payment-term-persistence-mappers.di";
import { buildPaymentTermRepository } from "./payment-term-repositories.di";
export type PaymentTermsInternalDeps = {
repository: IPaymentTermRepository;
useCases: {
listPaymentTerms: () => ListPaymentTermsUseCase;
getPaymentTermById: () => GetPaymentTermByIdUseCase;
createPaymentTerm: () => CreatePaymentTermUseCase;
updatePaymentTermById: () => UpdatePaymentTermByIdUseCase;
deletePaymentTermById: () => DeletePaymentTermByIdUseCase;
disablePaymentTermById: () => DisablePaymentTermByIdUseCase;
enablePaymentTermById: () => EnablePaymentTermByIdUseCase;
};
};
export const buildPaymentTermsDependencies = (params: ModuleParams): PaymentTermsInternalDeps => {
const { database } = params;
const transactionManager = buildTransactionManager(database as Sequelize);
const persistenceMappers = buildPaymentTermPersistenceMappers();
const repository = buildPaymentTermRepository({ database, mappers: persistenceMappers });
const inputMappers = buildPaymentTermInputMappers();
const finder = buildPaymentTermFinder({ repository });
const creator = buildPaymentTermCreator({ repository });
const updater = buildPaymentTermUpdater({ repository });
const deleter = buildPaymentTermDeleter({ repository });
const statusChanger = buildPaymentTermStatusChanger({ repository });
const snapshotBuilders = buildPaymentTermSnapshotBuilders();
return {
repository,
useCases: {
listPaymentTerms: () =>
new ListPaymentTermsUseCase(finder, snapshotBuilders.summary, transactionManager),
getPaymentTermById: () =>
new GetPaymentTermByIdUseCase(finder, snapshotBuilders.full, transactionManager),
createPaymentTerm: () =>
new CreatePaymentTermUseCase({
dtoMapper: inputMappers.createInputMapper,
creator,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
updatePaymentTermById: () =>
new UpdatePaymentTermByIdUseCase({
updater,
finder,
dtoMapper: inputMappers.updateInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
deletePaymentTermById: () =>
new DeletePaymentTermByIdUseCase({
deleter,
finder,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
disablePaymentTermById: () =>
new DisablePaymentTermByIdUseCase({
finder,
changer: statusChanger,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
enablePaymentTermById: () =>
new EnablePaymentTermByIdUseCase({
finder,
changer: statusChanger,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
},
};
};
export const buildPaymentTermsPublicServices = (
_params: SetupParams,
deps: PaymentTermsInternalDeps
): { finder: PaymentTermFinder } => {
return {
finder: new PaymentTermFinder(deps.repository),
};
};

View File

@ -0,0 +1,39 @@
import type { CreatePaymentTermRequestDTO } from "@erp/catalogs/common";
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { CreatePaymentTermUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class CreatePaymentTermController extends ExpressController {
constructor(private readonly useCase: CreatePaymentTermUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const dto = this.req.body satisfies CreatePaymentTermRequestDTO;
const result = await this.useCase.execute({ dto, companyId });
return result.match(
(data: unknown) => this.created(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,38 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeletePaymentTermByIdUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class DeletePaymentTermByIdController extends ExpressController {
constructor(private readonly useCase: DeletePaymentTermByIdUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_term_id } = this.req.params;
const result = await this.useCase.execute({ payment_term_id, companyId });
return result.match(
(data: unknown) => this.ok(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,38 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DisablePaymentTermByIdUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class DisablePaymentTermByIdController extends ExpressController {
constructor(private readonly useCase: DisablePaymentTermByIdUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_term_id } = this.req.params;
const result = await this.useCase.execute({ payment_term_id, companyId });
return result.match(
(data: unknown) => this.ok(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,38 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { EnablePaymentTermByIdUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class EnablePaymentTermByIdController extends ExpressController {
constructor(private readonly useCase: EnablePaymentTermByIdUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_term_id } = this.req.params;
const result = await this.useCase.execute({ payment_term_id, companyId });
return result.match(
(data: unknown) => this.ok(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,38 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { GetPaymentTermByIdUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class GetPaymentTermByIdController extends ExpressController {
constructor(private readonly useCase: GetPaymentTermByIdUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_term_id } = this.req.params;
const result = await this.useCase.execute({ payment_term_id, companyId });
return result.match(
(data: unknown) => this.ok(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,7 @@
export * from "./create-payment-term.controller";
export * from "./delete-payment-term-by-id.controller";
export * from "./disable-payment-term-by-id.controller";
export * from "./enable-payment-term-by-id.controller";
export * from "./get-payment-term-by-id.controller";
export * from "./list-payment-terms.controller";
export * from "./update-payment-term-by-id.controller";

View File

@ -0,0 +1,54 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListPaymentTermsUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class ListPaymentTermsController extends ExpressController {
constructor(private readonly useCase: ListPaymentTermsUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
private getCriteriaWithDefaultOrder() {
if (this.criteria.hasOrder()) {
return this.criteria;
}
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
return Criteria.fromPrimitives(filters, "name", "ASC", pageSize, pageNumber, quicksearch);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const criteria = this.getCriteriaWithDefaultOrder();
const result = await this.useCase.execute({ criteria, companyId });
return result.match(
(data: any) =>
this.ok(data, {
"X-Total-Count": String(data.total_items),
"Pagination-Count": String(data.total_pages),
"Pagination-Page": String(data.page),
"Pagination-Limit": String(data.per_page),
}),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,44 @@
import type { UpdatePaymentTermByIdRequestDTO } from "@erp/catalogs/common";
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { UpdatePaymentTermByIdUseCase } from "../../../../application/payment-terms";
import { paymentTermsApiErrorMapper } from "../payment-terms-api-error-mapper";
export class UpdatePaymentTermByIdController extends ExpressController {
constructor(private readonly useCase: UpdatePaymentTermByIdUseCase) {
super();
this.errorMapper = paymentTermsApiErrorMapper;
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_term_id } = this.req.params;
if (!payment_term_id) {
return this.invalidInputError("Payment term ID missing");
}
const dto = this.req.body as UpdatePaymentTermByIdRequestDTO;
const result = await this.useCase.execute({ payment_term_id, companyId, dto });
return result.match(
(data: unknown) => this.ok(data),
(err: Error) => this.handleError(err)
);
}
}

View File

@ -0,0 +1 @@
export * from "./payment-terms.routes";

View File

@ -0,0 +1,182 @@
import {
ApiErrorMapper,
ConflictApiError,
type ErrorToApiRule,
NotFoundApiError,
ValidationApiError,
} from "@erp/core/api";
import {
type InvalidPaymentTermDescriptionError,
type InvalidPaymentTermDueDaysError,
type InvalidPaymentTermDueRulesError,
type InvalidPaymentTermIdError,
type InvalidPaymentTermNameError,
type InvalidPaymentTermPercentageError,
type PaymentTermCannotBeDeletedError,
type PaymentTermCannotBeDisabledError,
type PaymentTermCannotBeEnabledError,
type PaymentTermCannotBeUpdatedError,
type PaymentTermDueDaysDuplicatedError,
type PaymentTermDueRuleMismatch,
type PaymentTermNotFoundError,
type PaymentTermPercentageSumMismatchError,
isInvalidPaymentTermDescriptionError,
isInvalidPaymentTermDueDaysError,
isInvalidPaymentTermDueRulesError,
isInvalidPaymentTermIdError,
isInvalidPaymentTermNameError,
isInvalidPaymentTermPercentageError,
isPaymentTermCannotBeDeletedError,
isPaymentTermCannotBeDisabledError,
isPaymentTermCannotBeEnabledError,
isPaymentTermCannotBeUpdatedError,
isPaymentTermDueDaysDuplicatedError,
isPaymentTermDueRuleMismatch,
isPaymentTermNotFoundError,
isPaymentTermPercentageSumMismatchError,
} from "../../../domain/payment-terms";
const invalidPaymentTermIdRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermIdError,
build: (error) =>
new ConflictApiError(
(error as InvalidPaymentTermIdError).message ||
"Payment term with the provided id already exists."
),
};
const invalidPaymentTermNameRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermNameError,
build: (error) =>
new ValidationApiError(
(error as InvalidPaymentTermNameError).message || "Payment term name is invalid."
),
};
const invalidPaymentTermDescriptionRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermDescriptionError,
build: (error) =>
new ValidationApiError(
(error as InvalidPaymentTermDescriptionError).message ||
"Payment term description is invalid."
),
};
const invalidPaymentTermDueDaysRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermDueDaysError,
build: (error) =>
new ValidationApiError(
(error as InvalidPaymentTermDueDaysError).message || "Payment term due days are invalid."
),
};
const invalidPaymentTermPercentageRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermPercentageError,
build: (error) =>
new ValidationApiError(
(error as InvalidPaymentTermPercentageError).message || "Payment term percentage is invalid."
),
};
const invalidPaymentTermDueRulesRule: ErrorToApiRule = {
priority: 120,
matches: isInvalidPaymentTermDueRulesError,
build: (error) =>
new ValidationApiError(
(error as InvalidPaymentTermDueRulesError).message || "Payment term due rules are invalid."
),
};
const paymentTermDueDaysDuplicatedRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermDueDaysDuplicatedError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermDueDaysDuplicatedError).message ||
"Payment term due days are duplicated."
),
};
const paymentTermPercentageSumMismatchRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermPercentageSumMismatchError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermPercentageSumMismatchError).message ||
"Payment term due rule percentages must sum 100.00."
),
};
const paymentTermNotFoundRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermNotFoundError,
build: (error) =>
new NotFoundApiError((error as PaymentTermNotFoundError).message || "Payment term not found."),
};
const paymentTermCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermCannotBeDeletedError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermCannotBeDeletedError).message || "Payment term cannot be deleted."
),
};
const paymentTermCannotBeDisabledRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermCannotBeDisabledError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermCannotBeDisabledError).message || "Payment term cannot be disabled."
),
};
const paymentTermCannotBeEnabledRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermCannotBeEnabledError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermCannotBeEnabledError).message || "Payment term cannot be enabled."
),
};
const paymentTermCannotBeUpdatedRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermCannotBeUpdatedError,
build: (error) =>
new ValidationApiError(
(error as PaymentTermCannotBeUpdatedError).message || "Payment term cannot be updated."
),
};
const paymentTermDueRuleMismatchRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentTermDueRuleMismatch,
build: (error) =>
new ValidationApiError(
(error as PaymentTermDueRuleMismatch).message || "Payment term due rule is mismatched."
),
};
export const paymentTermsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invalidPaymentTermIdRule)
.register(invalidPaymentTermNameRule)
.register(invalidPaymentTermDescriptionRule)
.register(invalidPaymentTermDueDaysRule)
.register(invalidPaymentTermPercentageRule)
.register(invalidPaymentTermDueRulesRule)
.register(paymentTermDueDaysDuplicatedRule)
.register(paymentTermPercentageSumMismatchRule)
.register(paymentTermNotFoundRule)
.register(paymentTermCannotBeDeletedRule)
.register(paymentTermCannotBeDisabledRule)
.register(paymentTermCannotBeEnabledRule)
.register(paymentTermCannotBeUpdatedRule)
.register(paymentTermDueRuleMismatchRule);

View File

@ -0,0 +1,104 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import type { NextFunction, Request, Response } from "express";
import { Router } from "express";
import {
CreatePaymentTermRequestSchema,
DeletePaymentTermByIdRequestSchema,
GetPaymentTermByIdRequestSchema,
ListPaymentTermsRequestSchema,
UpdatePaymentTermByIdParamsRequestSchema,
UpdatePaymentTermByIdRequestSchema,
} from "../../../../common";
import type { CatalogsInternalDeps } from "../../di";
import {
CreatePaymentTermController,
DeletePaymentTermByIdController,
DisablePaymentTermByIdController,
EnablePaymentTermByIdController,
GetPaymentTermByIdController,
ListPaymentTermsController,
UpdatePaymentTermByIdController,
} from "./controllers";
export const paymentTermsRouter = (params: StartParams) => {
const { app, config, getInternal } = params;
const deps = getInternal<CatalogsInternalDeps>("catalogs").paymentTerms;
const router = Router({ mergeParams: true });
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
router.use((req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next)
);
}
router.use([
(req: Request, res: Response, next: NextFunction) =>
requireAuthenticated()(req as RequestWithAuth, res, next),
(req: Request, res: Response, next: NextFunction) =>
requireCompanyContext()(req as RequestWithAuth, res, next),
]);
router.get("/", validateRequest(ListPaymentTermsRequestSchema, "query"), (req, res, next) => {
const controller = new ListPaymentTermsController(deps.useCases.listPaymentTerms());
return controller.execute(req, res, next);
});
router.post("/", validateRequest(CreatePaymentTermRequestSchema, "body"), (req, res, next) => {
const controller = new CreatePaymentTermController(deps.useCases.createPaymentTerm());
return controller.execute(req, res, next);
});
router.get(
"/:payment_term_id",
validateRequest(GetPaymentTermByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new GetPaymentTermByIdController(deps.useCases.getPaymentTermById());
return controller.execute(req, res, next);
}
);
router.delete(
"/:payment_term_id",
validateRequest(DeletePaymentTermByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new DeletePaymentTermByIdController(deps.useCases.deletePaymentTermById());
return controller.execute(req, res, next);
}
);
router.put(
"/:payment_term_id",
validateRequest(UpdatePaymentTermByIdParamsRequestSchema, "params"),
validateRequest(UpdatePaymentTermByIdRequestSchema, "body"),
(req, res, next) => {
const controller = new UpdatePaymentTermByIdController(deps.useCases.updatePaymentTermById());
return controller.execute(req, res, next);
}
);
router.patch(
"/:payment_term_id/disable",
validateRequest(GetPaymentTermByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new DisablePaymentTermByIdController(
deps.useCases.disablePaymentTermById()
);
return controller.execute(req, res, next);
}
);
router.patch(
"/:payment_term_id/enable",
validateRequest(GetPaymentTermByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new EnablePaymentTermByIdController(deps.useCases.enablePaymentTermById());
return controller.execute(req, res, next);
}
);
app.use(`${config.server.apiBasePath}/catalogs/payment-terms`, router);
};

View File

@ -0,0 +1,2 @@
export * from "./express";
export * from "./persistence";

View File

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

View File

@ -0,0 +1,8 @@
export * from "./mappers";
export * from "./models";
export * from "./repositories";
import paymentTermModelInit from "./models/sequelize-payment-term.model";
import paymentTermDueRuleModelInit from "./models/sequelize-payment-term-due-rule.model";
export const paymentTermModels = [paymentTermModelInit, paymentTermDueRuleModelInit];

View File

@ -0,0 +1,3 @@
export * from "./sequelize-payment-term-domain.mapper";
export * from "./sequelize-payment-term-due-rule-domain.mapper";
export * from "./sequelize-payment-term-summary.mapper";

View File

@ -0,0 +1,129 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
Name,
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { PaymentTerm, PaymentTermDueRules } from "../../../../../domain";
import type { PaymentTermCreationAttributes, PaymentTermModel } from "../models";
import { SequelizePaymentTermDueRuleDomainMapper } from "./sequelize-payment-term-due-rule-domain.mapper";
export class SequelizePaymentTermDomainMapper extends SequelizeDomainMapper<
PaymentTermModel,
PaymentTermCreationAttributes,
PaymentTerm
> {
private _dueRulesMapper: SequelizePaymentTermDueRuleDomainMapper;
constructor() {
super();
this._dueRulesMapper = new SequelizePaymentTermDueRuleDomainMapper();
}
public mapToDomain(raw: PaymentTermModel, params?: MapperParamsType): Result<PaymentTerm, Error> {
try {
const errors: ValidationErrorDetail[] = [];
const idResult = UniqueID.create(raw.id, true);
if (idResult.isFailure) {
return Result.fail(idResult.error);
}
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const name = extractOrPushError(Name.create(raw.name), "name", errors);
const description = extractOrPushError(
maybeFromNullableResult(raw.description, (value) => TextValue.create(value)),
"description",
errors
);
const dueCollectionResults = this._dueRulesMapper.mapToDomainCollection(
raw.due_rules,
raw.due_rules.length,
{
errors,
parent: raw,
...params,
}
);
if (dueCollectionResults.isFailure) {
errors.push({ path: "due_rules", message: dueCollectionResults.error.message });
}
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("PaymentTerm mapping failed [mapToDTO]", errors)
);
}
// 6) Construcción del agregado (Dominio)
const dueRulesResult = PaymentTermDueRules.create({
rules: dueCollectionResults.data.getAll(),
});
const paymentTerm = PaymentTerm.rehydrate(
{
companyId: companyId!,
name: name!,
description: description!,
isActive: raw.is_active,
isSystem: raw.is_system,
},
dueRulesResult,
idResult.data
);
return Result.ok(paymentTerm);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
public mapToPersistence(
source: PaymentTerm,
params?: MapperParamsType
): Result<PaymentTermCreationAttributes, Error> {
const errors: ValidationErrorDetail[] = [];
// 1) Items
const dueRulesResult = this._dueRulesMapper.mapToPersistenceArray(source.dueRules, {
errors,
parent: source,
...params,
});
if (dueRulesResult.isFailure) {
errors.push({
path: "dueRules",
message: dueRulesResult.error.message,
});
}
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Payment term mapping to persistence failed", errors)
);
}
const dueRules = dueRulesResult.data;
return Result.ok<PaymentTermCreationAttributes>({
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
name: source.name.toPrimitive(),
description: maybeToNullable(source.description, (value: TextValue) => value.toPrimitive()),
is_active: source.isActive,
is_system: source.isSystem,
due_rules: dueRules,
});
}
}

View File

@ -0,0 +1,104 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
type IPaymentTermCreateProps,
type PaymentTerm,
PaymentTermDueDays,
PaymentTermDueRule,
PaymentTermPercentage,
} from "../../../../../domain/payment-terms";
import type { PaymentTermDueRuleCreationAttributes, PaymentTermDueRuleModel } from "../models";
/**
* Mapper para payment_term_due_rules
*
* Domina estructuras:
* {
* dueDays: PaymentTermDueDays
* percentage: PaymentTermPercentage
* }
*
* Cada fila = una regla de vencimiento (cuándo pagar qué porcentaje).
*/
export class SequelizePaymentTermDueRuleDomainMapper extends SequelizeDomainMapper<
PaymentTermDueRuleModel,
PaymentTermDueRuleCreationAttributes,
PaymentTermDueRule
> {
public mapToDomain(
source: PaymentTermDueRuleModel,
params?: MapperParamsType
): Result<PaymentTermDueRule, Error> {
const { errors, index } = params as {
index: number;
errors: ValidationErrorDetail[];
parent: Partial<IPaymentTermCreateProps>;
};
const dueId = extractOrPushError(
UniqueID.create(source.due_id),
`due_rules[${index}].due_id`,
errors
);
const dueDaysResult = extractOrPushError(
PaymentTermDueDays.create(source.due_days),
`due_rules${index}.due_days`,
errors
);
const percentageResult = extractOrPushError(
PaymentTermPercentage.create({
value: Number(source.percentage_value ?? 0),
scale: Number(source.percentage_scale ?? 2),
}),
`due_rules${index}.percentage`,
errors
);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Payment term due rule mapping failed [mapToDomain]", errors)
);
}
const dueRuleResult = PaymentTermDueRule.rehydrate(
{
dueDays: dueDaysResult!,
percentage: percentageResult!,
},
dueId!
);
return Result.ok(dueRuleResult);
}
public mapToPersistence(
source: PaymentTermDueRule,
params?: MapperParamsType
): Result<PaymentTermDueRuleCreationAttributes, Error> {
const { errors, index, parent } = params as {
index: number;
parent: PaymentTerm;
errors: ValidationErrorDetail[];
};
const dto: PaymentTermDueRuleCreationAttributes = {
due_id: source.id.toPrimitive(),
payment_term_id: parent.id.toPrimitive(),
due_days: source.dueDays.toPrimitive(),
percentage_value: source.percentage.value,
percentage_scale: source.percentage.scale,
};
return Result.ok(dto);
}
}

View File

@ -0,0 +1,68 @@
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
Name,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentTermSummary } from "../../../../../application/payment-terms";
import type { PaymentTermModel } from "../models";
import { SequelizePaymentTermDueRuleDomainMapper } from "./sequelize-payment-term-due-rule-domain.mapper";
export class SequelizePaymentTermSummaryMapper extends SequelizeQueryMapper<
PaymentTermModel,
PaymentTermSummary
> {
private _dueRulesMapper: SequelizePaymentTermDueRuleDomainMapper;
constructor() {
super();
this._dueRulesMapper = new SequelizePaymentTermDueRuleDomainMapper();
}
public mapToReadModel(
raw: PaymentTermModel,
params?: MapperParamsType
): Result<PaymentTermSummary, Error> {
const errors: ValidationErrorDetail[] = [];
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const name = extractOrPushError(Name.create(raw.name), "name", errors);
const dueCollectionResults = this._dueRulesMapper.mapToDomainCollection(
raw.due_rules,
raw.due_rules.length,
{
errors,
parent: raw,
...params,
}
);
if (dueCollectionResults.isFailure) {
errors.push({ path: "due_rules", message: dueCollectionResults.error.message });
}
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("PaymentTerm mapping failed [mapToDTO]", errors)
);
}
const dueRules = dueCollectionResults.data.getAll();
return Result.ok<PaymentTermSummary>({
id: id!,
companyId: companyId!,
name: name!,
isActive: raw.is_active,
isSystem: raw.is_system,
dueRules,
});
}
}

View File

@ -0,0 +1,3 @@
export * from "./sequelize-payment-term.model";
export * from "./sequelize-payment-term-due-rule.model";
export * from "./sequelize-payment-term-due-rule.model";

View File

@ -0,0 +1,104 @@
import {
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type NonAttribute,
type Sequelize,
} from "sequelize";
import type { PaymentTermModel } from "./sequelize-payment-term.model";
export type PaymentTermDueRuleCreationAttributes = InferCreationAttributes<
PaymentTermDueRuleModel,
{ omit: "payment_term" }
>;
export class PaymentTermDueRuleModel extends Model<
InferAttributes<PaymentTermDueRuleModel>,
InferCreationAttributes<PaymentTermDueRuleModel, { omit: "payment_term" }>
> {
declare due_id: string;
declare payment_term_id: string;
declare due_days: number;
declare percentage_value: number;
declare percentage_scale: number;
// Relaciones
declare payment_term: NonAttribute<PaymentTermModel>;
static associate(database: Sequelize) {
const models = database.models;
const requiredModels = ["PaymentTermDueRuleModel", "PaymentTermModel"];
// Comprobamos que los modelos existan
for (const name of requiredModels) {
if (!models[name]) {
throw new Error(`[PaymentTermDueRuleModel.associate] Missing model: ${name}`);
}
}
const { PaymentTermDueRuleModel, PaymentTermModel } = models;
PaymentTermDueRuleModel.belongsTo(PaymentTermModel, {
as: "payment_term",
foreignKey: "payment_term_id",
targetKey: "id",
constraints: true,
onDelete: "CASCADE",
onUpdate: "CASCADE",
});
}
static hooks(_database: Sequelize) {
// No hooks required for due rule persistence
}
}
export default (database: Sequelize) => {
PaymentTermDueRuleModel.init(
{
due_id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
payment_term_id: {
type: DataTypes.UUID,
allowNull: false,
},
due_days: {
type: DataTypes.SMALLINT.UNSIGNED,
allowNull: false,
},
percentage_value: {
type: DataTypes.SMALLINT.UNSIGNED,
allowNull: false,
},
percentage_scale: {
type: DataTypes.SMALLINT.UNSIGNED,
allowNull: false,
defaultValue: 2,
},
},
{
sequelize: database,
modelName: "PaymentTermDueRuleModel",
tableName: "payment_term_due_rules",
underscored: true,
indexes: [
{
name: "idx_payment_term_due_rules_payment_term",
fields: ["payment_term_id"],
},
],
defaultScope: {},
scopes: {},
}
);
return PaymentTermDueRuleModel;
};

View File

@ -0,0 +1,121 @@
import {
type CreationOptional,
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type NonAttribute,
type Sequelize,
} from "sequelize";
import type {
PaymentTermDueRuleCreationAttributes,
PaymentTermDueRuleModel,
} from "./sequelize-payment-term-due-rule.model";
export type PaymentTermCreationAttributes = InferCreationAttributes<
PaymentTermModel,
{ omit: "due_rules" }
> & {
due_rules?: PaymentTermDueRuleCreationAttributes[];
};
export class PaymentTermModel extends Model<
InferAttributes<PaymentTermModel>,
InferCreationAttributes<PaymentTermModel, { omit: "due_rules" }>
> {
declare id: string;
declare company_id: string;
declare name: string;
declare description: CreationOptional<string | null>;
declare is_active: boolean;
declare is_system: boolean;
// Relaciones
declare due_rules: NonAttribute<PaymentTermDueRuleModel[]>;
static associate(database: Sequelize) {
const models = database.models;
const requiredModels = ["PaymentTermDueRuleModel", "PaymentTermModel"];
// Comprobamos que los modelos existan
for (const name of requiredModels) {
if (!models[name]) {
throw new Error(`[PaymentTermModel.associate] Missing model: ${name}`);
}
}
// Los modelos existen
const { PaymentTermDueRuleModel } = models;
PaymentTermModel.hasMany(PaymentTermDueRuleModel, {
as: "due_rules",
foreignKey: "payment_term_id",
sourceKey: "id",
constraints: true,
onDelete: "CASCADE",
onUpdate: "CASCADE",
});
}
static hooks(_database: Sequelize) {}
}
export default (database: Sequelize) => {
PaymentTermModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
company_id: {
type: DataTypes.UUID,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.STRING,
allowNull: true,
},
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
is_system: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
},
{
sequelize: database,
modelName: "PaymentTermModel",
tableName: "payment_terms",
underscored: true,
paranoid: true,
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{
name: "idx_payment_terms_company",
fields: ["company_id", "deleted_at", "name"],
},
],
whereMergeStrategy: "and",
defaultScope: {},
scopes: {},
}
);
return PaymentTermModel;
};

View File

@ -0,0 +1 @@
export * from "./sequelize-payment-term.repository";

View File

@ -0,0 +1,171 @@
import {
EntityNotFoundError,
InfrastructureRepositoryError,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import type { PaymentTermSummary } from "../../../../../application/payment-terms/models/payment-term-summary.model";
import type { IPaymentTermRepository } from "../../../../../application/payment-terms/repositories/payment-term-repository.interface";
import type { PaymentTerm } from "../../../../../domain";
import type {
SequelizePaymentTermDomainMapper,
SequelizePaymentTermSummaryMapper,
} from "../mappers";
import { PaymentTermModel } from "../models";
export class SequelizePaymentTermRepository
extends SequelizeRepository<PaymentTerm>
implements IPaymentTermRepository
{
constructor(
private readonly domainMapper: SequelizePaymentTermDomainMapper,
private readonly summaryMapper: SequelizePaymentTermSummaryMapper,
database: Sequelize
) {
super({ database });
}
async create(paymentTerm: PaymentTerm, transaction?: Transaction): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(paymentTerm);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}
await PaymentTermModel.create(dtoResult.data, { include: [{ all: true }], transaction });
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async update(paymentTerm: PaymentTerm, transaction?: Transaction): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(paymentTerm);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}
const { id, ...payload } = dtoResult.data;
const [affected] = await PaymentTermModel.update(payload, {
where: { id },
transaction,
individualHooks: true,
});
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or payment term not found")
);
}
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await PaymentTermModel.count({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok(Boolean(count > 0));
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<PaymentTerm, Error>> {
try {
const row = await PaymentTermModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
include: [{ all: true }],
});
if (!row) {
return Result.fail(new EntityNotFoundError("PaymentTerm", "id", id.toString()));
}
return this.domainMapper.mapToDomain(row);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<PaymentTermSummary>, Error>> {
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
searchableFields: [],
sortableFields: ["name"],
enableFullText: true,
database: this.database,
strictMode: true,
});
query.where = {
...query.where,
company_id: companyId.toString(),
deleted_at: null,
};
const [rows, count] = await Promise.all([
PaymentTermModel.findAll({
...query,
transaction,
include: [{ all: true }],
}),
PaymentTermModel.count({
where: query.where,
distinct: true,
transaction,
}),
]);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const deleted = await PaymentTermModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (deleted === 0) {
return Result.fail(new EntityNotFoundError("PaymentTerm", "id", id.toString()));
}
return Result.ok(true);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
}

View File

@ -1 +1,2 @@
export * from "./payment-methods";
export * from "./payment-terms";

View File

@ -0,0 +1,3 @@
export * from "./request";
export * from "./response";
export * from "./shared";

View File

@ -0,0 +1,13 @@
import { z } from "zod/v4";
import { PaymentTermDueRuleSchema } from "../shared/payment-term-due-rule.dto";
export const CreatePaymentTermRequestSchema = z.object({
id: z.uuid(),
name: z.string(),
description: z.string().nullable().optional(),
is_active: z.boolean(),
due_rules: z.array(PaymentTermDueRuleSchema),
});
export type CreatePaymentTermRequestDTO = z.infer<typeof CreatePaymentTermRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const DeletePaymentTermByIdRequestSchema = z.object({
payment_term_id: z.uuid(),
});
export type DeletePaymentTermByIdRequestDTO = z.infer<typeof DeletePaymentTermByIdRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const GetPaymentTermByIdRequestSchema = z.object({
payment_term_id: z.uuid(),
});
export type GetPaymentTermByIdRequestDTO = z.infer<typeof GetPaymentTermByIdRequestSchema>;

View File

@ -0,0 +1,5 @@
export * from "./create-payment-term.request.dto";
export * from "./delete-payment-term-by-id.request.dto";
export * from "./get-payment-term-by-id.request.dto";
export * from "./list-payment-terms.request.dto";
export * from "./update-payment-term-by-id.request.dto";

Some files were not shown because too many files have changed in this diff Show More