Catalogos
This commit is contained in:
parent
2a1d49d1c4
commit
567c9d9730
@ -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);
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
|
||||
@ -42,7 +42,7 @@ export class CreatePaymentMethodInputMapper implements ICreatePaymentMethodInput
|
||||
|
||||
this.throwIfValidationErrors(errors);
|
||||
|
||||
const props = {
|
||||
const props: IPaymentMethodCreateProps = {
|
||||
companyId: params.companyId,
|
||||
name: name!,
|
||||
description: description!,
|
||||
|
||||
@ -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";
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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";
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./create-payment-term-input.mapper";
|
||||
export * from "./update-payment-term-by-id-input.mapper";
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./payment-term-summary.model";
|
||||
@ -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>;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from './payment-term-repository.interface';
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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';
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import type { IPaymentTermFinder } from "./payment-term-finder";
|
||||
|
||||
export interface IPaymentTermPublicServices {
|
||||
finder: IPaymentTermFinder;
|
||||
}
|
||||
|
||||
export const buildPaymentTermPublicServices = (
|
||||
finder: IPaymentTermFinder
|
||||
): IPaymentTermPublicServices => ({
|
||||
finder,
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./payment-term-full-snapshot-builder";
|
||||
@ -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(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./full";
|
||||
export * from "./summary";
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./payment-term-summary-snapshot-builder";
|
||||
@ -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(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
|
||||
126
modules/catalogs/src/api/domain/payment-terms/errors.ts
Normal file
126
modules/catalogs/src/api/domain/payment-terms/errors.ts
Normal 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;
|
||||
7
modules/catalogs/src/api/domain/payment-terms/index.ts
Normal file
7
modules/catalogs/src/api/domain/payment-terms/index.ts
Normal 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";
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
import { Name } from "@repo/rdx-ddd";
|
||||
|
||||
export const PaymentTermName = Name;
|
||||
export type PaymentTermName = Name;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
};
|
||||
};
|
||||
1
modules/catalogs/src/api/infrastructure/di/index.ts
Normal file
1
modules/catalogs/src/api/infrastructure/di/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./catalogs.di.ts";
|
||||
@ -1 +1,2 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -1 +1 @@
|
||||
export * from "./payment-methods/payment-methods.routes";
|
||||
export * from "./payment-methods.routes";
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
isPaymentMethodCannotBeEnabledError,
|
||||
isPaymentMethodCannotBeUpdatedError,
|
||||
isPaymentMethodNotFoundError,
|
||||
} from "../../../../domain/payment-methods";
|
||||
} from "../../../domain/payment-methods";
|
||||
|
||||
const invalidPaymentMethodIdRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
ListPaymentMethodsRequestSchema,
|
||||
UpdatePaymentMethodByIdParamsRequestSchema,
|
||||
UpdatePaymentMethodByIdRequestSchema,
|
||||
} from "../../../../../common";
|
||||
} from "../../../../common";
|
||||
import type { CatalogsInternalDeps } from "../../di/catalogs.di";
|
||||
|
||||
import {
|
||||
@ -4,4 +4,4 @@ export * from "./repositories";
|
||||
|
||||
import paymentMethodModelInit from "./models/sequelize-payment-method.model";
|
||||
|
||||
export const models = [paymentMethodModelInit];
|
||||
export const paymentMethodModels = [paymentMethodModelInit];
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./payment-term-persistence-mappers.di";
|
||||
export * from "./payment-term-repositories.di";
|
||||
export * from "./payment-terms.di";
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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),
|
||||
};
|
||||
};
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./payment-terms.routes";
|
||||
@ -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);
|
||||
@ -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);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./express";
|
||||
export * from "./persistence";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize";
|
||||
@ -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];
|
||||
@ -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";
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-payment-term.repository";
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
|
||||
3
modules/catalogs/src/common/dto/payment-terms/index.ts
Normal file
3
modules/catalogs/src/common/dto/payment-terms/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./request";
|
||||
export * from "./response";
|
||||
export * from "./shared";
|
||||
@ -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>;
|
||||
@ -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>;
|
||||
@ -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>;
|
||||
@ -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
Loading…
Reference in New Issue
Block a user