diff --git a/apps/server/src/register-modules.ts b/apps/server/src/register-modules.ts index 2d149a87..b66b77b4 100644 --- a/apps/server/src/register-modules.ts +++ b/apps/server/src/register-modules.ts @@ -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); diff --git a/modules/catalogs/src/api/application/index.ts b/modules/catalogs/src/api/application/index.ts index cd29a5d4..7a4fc37c 100644 --- a/modules/catalogs/src/api/application/index.ts +++ b/modules/catalogs/src/api/application/index.ts @@ -1 +1,2 @@ export * from "./payment-methods"; +export * from "./payment-terms"; diff --git a/modules/catalogs/src/api/application/payment-methods/mappers/create-payment-method-input.mapper.ts b/modules/catalogs/src/api/application/payment-methods/mappers/create-payment-method-input.mapper.ts index 61b8bd78..43cb1299 100644 --- a/modules/catalogs/src/api/application/payment-methods/mappers/create-payment-method-input.mapper.ts +++ b/modules/catalogs/src/api/application/payment-methods/mappers/create-payment-method-input.mapper.ts @@ -42,7 +42,7 @@ export class CreatePaymentMethodInputMapper implements ICreatePaymentMethodInput this.throwIfValidationErrors(errors); - const props = { + const props: IPaymentMethodCreateProps = { companyId: params.companyId, name: name!, description: description!, diff --git a/modules/catalogs/src/api/application/payment-terms/di/index.ts b/modules/catalogs/src/api/application/payment-terms/di/index.ts new file mode 100644 index 00000000..8724d118 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-creator.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-creator.di.ts new file mode 100644 index 00000000..07929179 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-creator.di.ts @@ -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); +}; diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-deleter.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-deleter.di.ts new file mode 100644 index 00000000..9cb5e606 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-deleter.di.ts @@ -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); +}; diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-finder.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-finder.di.ts new file mode 100644 index 00000000..b9de9e0e --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-finder.di.ts @@ -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); +} diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-input-mappers.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-input-mappers.di.ts new file mode 100644 index 00000000..1155225c --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-input-mappers.di.ts @@ -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, + }; +}; diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-snapshot-builders.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-snapshot-builders.di.ts new file mode 100644 index 00000000..2bd248ec --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-snapshot-builders.di.ts @@ -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, + }; +} diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-status-changer.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-status-changer.di.ts new file mode 100644 index 00000000..9dc29207 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-status-changer.di.ts @@ -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); +}; diff --git a/modules/catalogs/src/api/application/payment-terms/di/payment-term-updater.di.ts b/modules/catalogs/src/api/application/payment-terms/di/payment-term-updater.di.ts new file mode 100644 index 00000000..cfd84f58 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/di/payment-term-updater.di.ts @@ -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); +}; diff --git a/modules/catalogs/src/api/application/payment-terms/index.ts b/modules/catalogs/src/api/application/payment-terms/index.ts new file mode 100644 index 00000000..b68ea0dc --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/application/payment-terms/mappers/create-payment-term-input.mapper.ts b/modules/catalogs/src/api/application/payment-terms/mappers/create-payment-term-input.mapper.ts new file mode 100644 index 00000000..7d40140a --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/mappers/create-payment-term-input.mapper.ts @@ -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, + 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 + ); + } + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/mappers/index.ts b/modules/catalogs/src/api/application/payment-terms/mappers/index.ts new file mode 100644 index 00000000..b3820569 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./create-payment-term-input.mapper"; +export * from "./update-payment-term-by-id-input.mapper"; diff --git a/modules/catalogs/src/api/application/payment-terms/mappers/update-payment-term-by-id-input.mapper.ts b/modules/catalogs/src/api/application/payment-terms/mappers/update-payment-term-by-id-input.mapper.ts new file mode 100644 index 00000000..2dc0e398 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/mappers/update-payment-term-by-id-input.mapper.ts @@ -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; +} + +/** + * @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 { + 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, + 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 + ); + } + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/models/index.ts b/modules/catalogs/src/api/application/payment-terms/models/index.ts new file mode 100644 index 00000000..aea5dcf8 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/models/index.ts @@ -0,0 +1 @@ +export * from "./payment-term-summary.model"; diff --git a/modules/catalogs/src/api/application/payment-terms/models/payment-term-summary.model.ts b/modules/catalogs/src/api/application/payment-terms/models/payment-term-summary.model.ts new file mode 100644 index 00000000..7ed1fcce --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/models/payment-term-summary.model.ts @@ -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; +}; diff --git a/modules/catalogs/src/api/application/payment-terms/repositories/index.ts b/modules/catalogs/src/api/application/payment-terms/repositories/index.ts new file mode 100644 index 00000000..e0a81431 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/repositories/index.ts @@ -0,0 +1 @@ +export * from './payment-term-repository.interface'; diff --git a/modules/catalogs/src/api/application/payment-terms/repositories/payment-term-repository.interface.ts b/modules/catalogs/src/api/application/payment-terms/repositories/payment-term-repository.interface.ts new file mode 100644 index 00000000..a2c913dd --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/repositories/payment-term-repository.interface.ts @@ -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>; + update(paymentTerm: PaymentTerm, transaction?: unknown): Promise>; + deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: unknown + ): Promise>; + existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; + findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>>; +} diff --git a/modules/catalogs/src/api/application/payment-terms/services/index.ts b/modules/catalogs/src/api/application/payment-terms/services/index.ts new file mode 100644 index 00000000..ba5f8de6 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/index.ts @@ -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'; diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-creator.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-creator.ts new file mode 100644 index 00000000..459f19ba --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-creator.ts @@ -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>; +} + +export class PaymentTermCreator implements IPaymentTermCreator { + constructor(private readonly repository: IPaymentTermRepository) {} + + public async create(params: IPaymentTermCreatorParams): Promise> { + 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); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-deleter.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-deleter.ts new file mode 100644 index 00000000..74e4d3ad --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-deleter.ts @@ -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>; +} + +export class PaymentTermDeleter implements IPaymentTermDeleter { + public constructor(private readonly repository: IPaymentTermRepository) {} + + public async delete(params: { + paymentTerm: PaymentTerm; + transaction?: unknown; + }): Promise> { + 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); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-finder.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-finder.ts new file mode 100644 index 00000000..50b41a72 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-finder.ts @@ -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>; + + paymentTermExists( + companyId: UniqueID, + paymentTermId: UniqueID, + transaction?: unknown + ): Promise>; + + findPaymentTermsByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>>; +} + +export class PaymentTermFinder implements IPaymentTermFinder { + constructor(private readonly repository: IPaymentTermRepository) {} + + public async findPaymentTermById( + companyId: UniqueID, + paymentTermId: UniqueID, + transaction?: unknown + ): Promise> { + return this.repository.getByIdInCompany(companyId, paymentTermId, transaction); + } + + public async paymentTermExists( + companyId: UniqueID, + paymentTermId: UniqueID, + transaction?: unknown + ): Promise> { + return this.repository.existsByIdInCompany(companyId, paymentTermId, transaction); + } + + public async findPaymentTermsByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>> { + return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-public-services.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-public-services.ts new file mode 100644 index 00000000..b90e4fae --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-public-services.ts @@ -0,0 +1,11 @@ +import type { IPaymentTermFinder } from "./payment-term-finder"; + +export interface IPaymentTermPublicServices { + finder: IPaymentTermFinder; +} + +export const buildPaymentTermPublicServices = ( + finder: IPaymentTermFinder +): IPaymentTermPublicServices => ({ + finder, +}); diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-status-changer.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-status-changer.ts new file mode 100644 index 00000000..cdb6d2a5 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-status-changer.ts @@ -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>; +} + +export class PaymentTermStatusChanger implements IPaymentTermStatusChanger { + public constructor(private readonly repository: IPaymentTermRepository) {} + + public async changeStatus(params: { + paymentTerm: PaymentTerm; + action: PaymentTermStatusChangeAction; + transaction?: unknown; + }): Promise> { + 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); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.ts b/modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.ts new file mode 100644 index 00000000..17f57701 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.ts @@ -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>; +} + +export class PaymentTermUpdater implements IPaymentTermUpdater { + constructor(private readonly repository: IPaymentTermRepository) {} + + public async update(params: { + companyId: UniqueID; + id: UniqueID; + patchProps: PaymentTermPatchProps; + transaction?: unknown; + }): Promise> { + 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); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/index.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/index.ts new file mode 100644 index 00000000..aae80073 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/index.ts @@ -0,0 +1 @@ +export * from "./payment-term-full-snapshot-builder"; diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/payment-term-full-snapshot-builder.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/payment-term-full-snapshot-builder.ts new file mode 100644 index 00000000..d40f7185 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/full/payment-term-full-snapshot-builder.ts @@ -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 {} + +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(), + })), + }; + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/index.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/index.ts new file mode 100644 index 00000000..3b83e1ff --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/index.ts @@ -0,0 +1,2 @@ +export * from "./full"; +export * from "./summary"; diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/payment-term-due-rule.snapshot-builder.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/payment-term-due-rule.snapshot-builder.ts new file mode 100644 index 00000000..4aa9e7b6 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/payment-term-due-rule.snapshot-builder.ts @@ -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(), + }; + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/index.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/index.ts new file mode 100644 index 00000000..f65f6a49 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/index.ts @@ -0,0 +1 @@ +export * from "./payment-term-summary-snapshot-builder"; diff --git a/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/payment-term-summary-snapshot-builder.ts b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/payment-term-summary-snapshot-builder.ts new file mode 100644 index 00000000..a6f9407d --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/snapshot-builders/summary/payment-term-summary-snapshot-builder.ts @@ -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 {} + +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(), + })), + }; + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/create-payment-term.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/create-payment-term.use-case.ts new file mode 100644 index 00000000..67821c02 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/create-payment-term.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/delete-payment-term-by-id.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/delete-payment-term-by-id.use-case.ts new file mode 100644 index 00000000..73e44a9c --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/delete-payment-term-by-id.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/disable-payment-term-by-id.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/disable-payment-term-by-id.use-case.ts new file mode 100644 index 00000000..5d62c374 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/disable-payment-term-by-id.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/enable-payment-term-by-id.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/enable-payment-term-by-id.use-case.ts new file mode 100644 index 00000000..a0404838 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/enable-payment-term-by-id.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/get-payment-term-by-id.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/get-payment-term-by-id.use-case.ts new file mode 100644 index 00000000..5a7dd413 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/get-payment-term-by-id.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/index.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/index.ts new file mode 100644 index 00000000..4e53434d --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/list-payment-terms.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/list-payment-terms.use-case.ts new file mode 100644 index 00000000..e70ff713 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/list-payment-terms.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/payment-terms/use-cases/update-payment-term-by-id.use-case.ts b/modules/catalogs/src/api/application/payment-terms/use-cases/update-payment-term-by-id.use-case.ts new file mode 100644 index 00000000..c06f4f47 --- /dev/null +++ b/modules/catalogs/src/api/application/payment-terms/use-cases/update-payment-term-by-id.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/catalogs/src/api/domain/index.ts b/modules/catalogs/src/api/domain/index.ts index cd29a5d4..7a4fc37c 100644 --- a/modules/catalogs/src/api/domain/index.ts +++ b/modules/catalogs/src/api/domain/index.ts @@ -1 +1,2 @@ export * from "./payment-methods"; +export * from "./payment-terms"; diff --git a/modules/catalogs/src/api/domain/payment-terms/errors.ts b/modules/catalogs/src/api/domain/payment-terms/errors.ts new file mode 100644 index 00000000..46d92975 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/errors.ts @@ -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; diff --git a/modules/catalogs/src/api/domain/payment-terms/index.ts b/modules/catalogs/src/api/domain/payment-terms/index.ts new file mode 100644 index 00000000..2cf02546 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term-due-days.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-days.ts new file mode 100644 index 00000000..eb33ef59 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-days.ts @@ -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 { + 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; + } +} diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rule.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rule.ts new file mode 100644 index 00000000..62703048 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rule.ts @@ -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 { + static create( + props: PaymentTermDueRuleCreateProps, + id?: UniqueID + ): Result { + 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 { + 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 + ); + } +} diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rules.collection.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rules.collection.ts new file mode 100644 index 00000000..ce5677be --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term-due-rules.collection.ts @@ -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 para no exponer +// públicamente métodos para manipular la colección. + +export type IPaymentTermDueRules = {}; + +export class PaymentTermDueRules + extends Collection + implements IPaymentTermDueRules +{ + // OJO, no extendemos de Collection 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(); + 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 }); + } +} diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term-name.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term-name.ts new file mode 100644 index 00000000..64b2ed9d --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term-name.ts @@ -0,0 +1,4 @@ +import { Name } from "@repo/rdx-ddd"; + +export const PaymentTermName = Name; +export type PaymentTermName = Name; diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term-percentage.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term-percentage.ts new file mode 100644 index 00000000..0661cac4 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term-percentage.ts @@ -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 { + 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; + } +} diff --git a/modules/catalogs/src/api/domain/payment-terms/payment-term.aggregate.ts b/modules/catalogs/src/api/domain/payment-terms/payment-term.aggregate.ts new file mode 100644 index 00000000..b85813b5 --- /dev/null +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term.aggregate.ts @@ -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; + isActive: boolean; + isSystem: boolean; + dueRules: PaymentTermDueRuleCreateProps[]; +} + +export type PaymentTermPatchProps = Partial> & { + dueRules?: PaymentTermDueRulePatchProps[]; +}; + +export type PaymentTermInternalProps = Omit; + +export class PaymentTerm extends AggregateRoot { + 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 { + 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 { + 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 { + if (Object.keys(patchProps).length === 0) { + return Result.ok(); + } + + return Result.ok(); + } + + private initializeRules( + rulesProps: PaymentTermDueRuleCreateProps[] | PaymentTermDueRulePatchProps[] + ): Result { + 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 { + 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 { + 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 { + 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 { + 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, next: Maybe): boolean { + return current.match( + (currentValue: TextValue) => + next.match( + (nextValue: TextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(), + () => false + ), + () => next.isNone() + ); + } +} diff --git a/modules/catalogs/src/api/index.ts b/modules/catalogs/src/api/index.ts index 4d40ab7a..23bc077b 100644 --- a/modules/catalogs/src/api/index.ts +++ b/modules/catalogs/src/api/index.ts @@ -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, diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/di/catalogs.di.ts b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts similarity index 64% rename from modules/catalogs/src/api/infrastructure/payment-methods/di/catalogs.di.ts rename to modules/catalogs/src/api/infrastructure/di/catalogs.di.ts index 2aa75e2f..39b10ca6 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/di/catalogs.di.ts +++ b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts @@ -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), }; }; diff --git a/modules/catalogs/src/api/infrastructure/di/index.ts b/modules/catalogs/src/api/infrastructure/di/index.ts new file mode 100644 index 00000000..34951646 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/di/index.ts @@ -0,0 +1 @@ +export * from "./catalogs.di.ts"; diff --git a/modules/catalogs/src/api/infrastructure/index.ts b/modules/catalogs/src/api/infrastructure/index.ts index cd29a5d4..7a4fc37c 100644 --- a/modules/catalogs/src/api/infrastructure/index.ts +++ b/modules/catalogs/src/api/infrastructure/index.ts @@ -1 +1,2 @@ export * from "./payment-methods"; +export * from "./payment-terms"; diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/di/index.ts b/modules/catalogs/src/api/infrastructure/payment-methods/di/index.ts index 500d6cd3..47499923 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/di/index.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/di/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/create-payment-method.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/create-payment-method.controller.ts similarity index 92% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/create-payment-method.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/create-payment-method.controller.ts index 6bfc6c76..88882eb2 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/create-payment-method.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/create-payment-method.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/delete-payment-method-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/delete-payment-method-by-id.controller.ts similarity index 98% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/delete-payment-method-by-id.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/delete-payment-method-by-id.controller.ts index fd2fba88..44ff8e24 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/delete-payment-method-by-id.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/delete-payment-method-by-id.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/disable-payment-method-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/disable-payment-method-by-id.controller.ts similarity index 98% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/disable-payment-method-by-id.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/disable-payment-method-by-id.controller.ts index 85753eaa..e321153a 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/disable-payment-method-by-id.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/disable-payment-method-by-id.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/enable-payment-method-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/enable-payment-method-by-id.controller.ts similarity index 98% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/enable-payment-method-by-id.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/enable-payment-method-by-id.controller.ts index 129607ae..ab3177a8 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/enable-payment-method-by-id.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/enable-payment-method-by-id.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/get-payment-method-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/get-payment-method-by-id.controller.ts similarity index 93% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/get-payment-method-by-id.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/get-payment-method-by-id.controller.ts index dea00d30..6fcb8766 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/get-payment-method-by-id.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/get-payment-method-by-id.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/index.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/index.ts similarity index 100% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/index.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/index.ts diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/list-payment-methods.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/list-payment-methods.controller.ts similarity index 95% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/list-payment-methods.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/list-payment-methods.controller.ts index 98537501..6c1c8476 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/list-payment-methods.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/list-payment-methods.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/update-payment-method-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/update-payment-method-by-id.controller.ts similarity index 98% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/update-payment-method-by-id.controller.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/update-payment-method-by-id.controller.ts index 45747de9..c01285c1 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/controllers/update-payment-method-by-id.controller.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/controllers/update-payment-method-by-id.controller.ts @@ -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 { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/index.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/index.ts index 1c7eb085..c1a705d4 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/index.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/index.ts @@ -1 +1 @@ -export * from "./payment-methods/payment-methods.routes"; +export * from "./payment-methods.routes"; diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods-api-error-mapper.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods-api-error-mapper.ts similarity index 98% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods-api-error-mapper.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods-api-error-mapper.ts index 695b9443..301e07f7 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods-api-error-mapper.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods-api-error-mapper.ts @@ -21,7 +21,7 @@ import { isPaymentMethodCannotBeEnabledError, isPaymentMethodCannotBeUpdatedError, isPaymentMethodNotFoundError, -} from "../../../../domain/payment-methods"; +} from "../../../domain/payment-methods"; const invalidPaymentMethodIdRule: ErrorToApiRule = { priority: 120, diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods.routes.ts b/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods.routes.ts similarity index 99% rename from modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods.routes.ts rename to modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods.routes.ts index ebc67c99..4f9c0194 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods/payment-methods.routes.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/express/payment-methods.routes.ts @@ -9,7 +9,7 @@ import { ListPaymentMethodsRequestSchema, UpdatePaymentMethodByIdParamsRequestSchema, UpdatePaymentMethodByIdRequestSchema, -} from "../../../../../common"; +} from "../../../../common"; import type { CatalogsInternalDeps } from "../../di/catalogs.di"; import { diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/index.ts b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/index.ts index e0e3ca2e..79324d82 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/index.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/index.ts @@ -4,4 +4,4 @@ export * from "./repositories"; import paymentMethodModelInit from "./models/sequelize-payment-method.model"; -export const models = [paymentMethodModelInit]; +export const paymentMethodModels = [paymentMethodModelInit]; diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts index 80a47a88..b11ae470 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository.ts @@ -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") diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/di/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/di/index.ts new file mode 100644 index 00000000..4e224651 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/di/index.ts @@ -0,0 +1,3 @@ +export * from "./payment-term-persistence-mappers.di"; +export * from "./payment-term-repositories.di"; +export * from "./payment-terms.di"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-persistence-mappers.di.ts b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-persistence-mappers.di.ts new file mode 100644 index 00000000..fcb657fc --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-persistence-mappers.di.ts @@ -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, + }; +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-repositories.di.ts b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-repositories.di.ts new file mode 100644 index 00000000..961c6456 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-term-repositories.di.ts @@ -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); +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-terms.di.ts b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-terms.di.ts new file mode 100644 index 00000000..954e5f6c --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/di/payment-terms.di.ts @@ -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), + }; +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/create-payment-term.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/create-payment-term.controller.ts new file mode 100644 index 00000000..53df2e4c --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/create-payment-term.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/delete-payment-term-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/delete-payment-term-by-id.controller.ts new file mode 100644 index 00000000..ab464eae --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/delete-payment-term-by-id.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/disable-payment-term-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/disable-payment-term-by-id.controller.ts new file mode 100644 index 00000000..101417ba --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/disable-payment-term-by-id.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/enable-payment-term-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/enable-payment-term-by-id.controller.ts new file mode 100644 index 00000000..d5bc9987 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/enable-payment-term-by-id.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/get-payment-term-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/get-payment-term-by-id.controller.ts new file mode 100644 index 00000000..211f8b70 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/get-payment-term-by-id.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/index.ts new file mode 100644 index 00000000..62848fc1 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/list-payment-terms.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/list-payment-terms.controller.ts new file mode 100644 index 00000000..de4eed17 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/list-payment-terms.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/update-payment-term-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/update-payment-term-by-id.controller.ts new file mode 100644 index 00000000..1de6e58d --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/controllers/update-payment-term-by-id.controller.ts @@ -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) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/index.ts new file mode 100644 index 00000000..bcc0a1fe --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/index.ts @@ -0,0 +1 @@ +export * from "./payment-terms.routes"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms-api-error-mapper.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms-api-error-mapper.ts new file mode 100644 index 00000000..21f5b6fb --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms-api-error-mapper.ts @@ -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); diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms.routes.ts b/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms.routes.ts new file mode 100644 index 00000000..93e54b51 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/express/payment-terms.routes.ts @@ -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("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); +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/index.ts new file mode 100644 index 00000000..4263e0ee --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/index.ts @@ -0,0 +1,2 @@ +export * from "./express"; +export * from "./persistence"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/index.ts new file mode 100644 index 00000000..86faa2c6 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/index.ts @@ -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]; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..d4efe9f8 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-domain.mapper.ts new file mode 100644 index 00000000..df463bc3 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-domain.mapper.ts @@ -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 { + 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 { + 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({ + 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, + }); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-due-rule-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-due-rule-domain.mapper.ts new file mode 100644 index 00000000..d93316f6 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-due-rule-domain.mapper.ts @@ -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 { + const { errors, index } = params as { + index: number; + errors: ValidationErrorDetail[]; + parent: Partial; + }; + + 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 { + 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); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-summary.mapper.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-summary.mapper.ts new file mode 100644 index 00000000..de43642a --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-summary.mapper.ts @@ -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 { + 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({ + id: id!, + companyId: companyId!, + name: name!, + isActive: raw.is_active, + isSystem: raw.is_system, + dueRules, + }); + } +} diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/index.ts new file mode 100644 index 00000000..38f9665b --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/index.ts @@ -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"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term-due-rule.model.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term-due-rule.model.ts new file mode 100644 index 00000000..979e2130 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term-due-rule.model.ts @@ -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, + InferCreationAttributes +> { + 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; + + 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; +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term.model.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term.model.ts new file mode 100644 index 00000000..27a6ecca --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term.model.ts @@ -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, + InferCreationAttributes +> { + declare id: string; + declare company_id: string; + declare name: string; + declare description: CreationOptional; + declare is_active: boolean; + declare is_system: boolean; + + // Relaciones + declare due_rules: NonAttribute; + + 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; +}; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/index.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..81ee3b19 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-payment-term.repository"; diff --git a/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/sequelize-payment-term.repository.ts b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/sequelize-payment-term.repository.ts new file mode 100644 index 00000000..7baf59a6 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/sequelize-payment-term.repository.ts @@ -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 + implements IPaymentTermRepository +{ + constructor( + private readonly domainMapper: SequelizePaymentTermDomainMapper, + private readonly summaryMapper: SequelizePaymentTermSummaryMapper, + database: Sequelize + ) { + super({ database }); + } + + async create(paymentTerm: PaymentTerm, transaction?: Transaction): Promise> { + 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> { + 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> { + 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> { + 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, 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> { + 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)); + } + } +} diff --git a/modules/catalogs/src/common/dto/index.ts b/modules/catalogs/src/common/dto/index.ts index cd29a5d4..7a4fc37c 100644 --- a/modules/catalogs/src/common/dto/index.ts +++ b/modules/catalogs/src/common/dto/index.ts @@ -1 +1,2 @@ export * from "./payment-methods"; +export * from "./payment-terms"; diff --git a/modules/catalogs/src/common/dto/payment-terms/index.ts b/modules/catalogs/src/common/dto/payment-terms/index.ts new file mode 100644 index 00000000..32497d2a --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/index.ts @@ -0,0 +1,3 @@ +export * from "./request"; +export * from "./response"; +export * from "./shared"; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/create-payment-term.request.dto.ts b/modules/catalogs/src/common/dto/payment-terms/request/create-payment-term.request.dto.ts new file mode 100644 index 00000000..f7ceb3ec --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/create-payment-term.request.dto.ts @@ -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; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/delete-payment-term-by-id.request.dto.ts b/modules/catalogs/src/common/dto/payment-terms/request/delete-payment-term-by-id.request.dto.ts new file mode 100644 index 00000000..becb8c65 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/delete-payment-term-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const DeletePaymentTermByIdRequestSchema = z.object({ + payment_term_id: z.uuid(), +}); + +export type DeletePaymentTermByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/get-payment-term-by-id.request.dto.ts b/modules/catalogs/src/common/dto/payment-terms/request/get-payment-term-by-id.request.dto.ts new file mode 100644 index 00000000..e5344052 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/get-payment-term-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const GetPaymentTermByIdRequestSchema = z.object({ + payment_term_id: z.uuid(), +}); + +export type GetPaymentTermByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/index.ts b/modules/catalogs/src/common/dto/payment-terms/request/index.ts new file mode 100644 index 00000000..56c5e9dd --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/index.ts @@ -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"; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/list-payment-terms.request.dto.ts b/modules/catalogs/src/common/dto/payment-terms/request/list-payment-terms.request.dto.ts new file mode 100644 index 00000000..36df2ae6 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/list-payment-terms.request.dto.ts @@ -0,0 +1,5 @@ +import { CriteriaSchema } from "@erp/core"; +import type { z } from "zod/v4"; + +export const ListPaymentTermsRequestSchema = CriteriaSchema; +export type ListPaymentTermsRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/request/update-payment-term-by-id.request.dto.ts b/modules/catalogs/src/common/dto/payment-terms/request/update-payment-term-by-id.request.dto.ts new file mode 100644 index 00000000..40bd9b76 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/request/update-payment-term-by-id.request.dto.ts @@ -0,0 +1,19 @@ +import { z } from "zod/v4"; + +import { PaymentTermDueRuleSchema } from "../shared/payment-term-due-rule.dto"; + +export const UpdatePaymentTermByIdParamsRequestSchema = z.object({ + payment_term_id: z.uuid(), +}); + +export const UpdatePaymentTermByIdRequestSchema = z.object({ + name: z.string().optional(), + description: z.string().nullable().optional(), + is_active: z.boolean().optional(), + due_rules: z.array(PaymentTermDueRuleSchema).optional(), +}); + +export type UpdatePaymentTermByIdParamsRequestDTO = z.infer< + typeof UpdatePaymentTermByIdParamsRequestSchema +>; +export type UpdatePaymentTermByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/response/create-payment-term.response.dto.ts b/modules/catalogs/src/common/dto/payment-terms/response/create-payment-term.response.dto.ts new file mode 100644 index 00000000..fc2eda0c --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/response/create-payment-term.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { PaymentTermDetailSchema } from "../shared/payment-term-detail.dto"; + +export const CreatePaymentTermResponseSchema = PaymentTermDetailSchema; +export type CreatePaymentTermResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/response/get-payment-term-by-id.response.dto.ts b/modules/catalogs/src/common/dto/payment-terms/response/get-payment-term-by-id.response.dto.ts new file mode 100644 index 00000000..f8a94a24 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/response/get-payment-term-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { PaymentTermDetailSchema } from "../shared/payment-term-detail.dto"; + +export const GetPaymentTermByIdResponseSchema = PaymentTermDetailSchema; +export type GetPaymentTermByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/response/index.ts b/modules/catalogs/src/common/dto/payment-terms/response/index.ts new file mode 100644 index 00000000..d76e0313 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/response/index.ts @@ -0,0 +1,4 @@ +export * from "./create-payment-term.response.dto"; +export * from "./get-payment-term-by-id.response.dto"; +export * from "./list-payment-terms.response.dto"; +export * from "./update-payment-term-by-id.response.dto"; diff --git a/modules/catalogs/src/common/dto/payment-terms/response/list-payment-terms.response.dto.ts b/modules/catalogs/src/common/dto/payment-terms/response/list-payment-terms.response.dto.ts new file mode 100644 index 00000000..fbca28b4 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/response/list-payment-terms.response.dto.ts @@ -0,0 +1,6 @@ +import { z } from "zod/v4"; + +import { PaymentTermSummarySchema } from "../shared/payment-term-summary.dto"; + +export const ListPaymentTermsResponseSchema = z.array(PaymentTermSummarySchema); +export type ListPaymentTermsResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/response/update-payment-term-by-id.response.dto.ts b/modules/catalogs/src/common/dto/payment-terms/response/update-payment-term-by-id.response.dto.ts new file mode 100644 index 00000000..161d5fba --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/response/update-payment-term-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { PaymentTermDetailSchema } from "../shared/payment-term-detail.dto"; + +export const UpdatePaymentTermByIdResponseSchema = PaymentTermDetailSchema; +export type UpdatePaymentTermByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/shared/index.ts b/modules/catalogs/src/common/dto/payment-terms/shared/index.ts new file mode 100644 index 00000000..6e7aeb73 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/shared/index.ts @@ -0,0 +1,3 @@ +export * from "./payment-term-detail.dto"; +export * from "./payment-term-due-rule.dto"; +export * from "./payment-term-summary.dto"; diff --git a/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-detail.dto.ts b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-detail.dto.ts new file mode 100644 index 00000000..ff9a9662 --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-detail.dto.ts @@ -0,0 +1,15 @@ +import { z } from "zod/v4"; + +import { PaymentTermDueRuleSchema } from "./payment-term-due-rule.dto"; + +export const PaymentTermDetailSchema = z.object({ + id: z.uuid(), + company_id: z.uuid(), + name: z.string(), + description: z.string().nullable(), + is_active: z.boolean(), + is_system: z.boolean(), + due_rules: z.array(PaymentTermDueRuleSchema), +}); + +export type PaymentTermDetailDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-due-rule.dto.ts b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-due-rule.dto.ts new file mode 100644 index 00000000..bcd9c07d --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-due-rule.dto.ts @@ -0,0 +1,9 @@ +import { NumericStringSchema, PercentageSchema } from "@erp/core"; +import { z } from "zod/v4"; + +export const PaymentTermDueRuleSchema = z.object({ + due_days: NumericStringSchema, + percentage: PercentageSchema, +}); + +export type PaymentTermDueRuleDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-summary.dto.ts b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-summary.dto.ts new file mode 100644 index 00000000..73c71fac --- /dev/null +++ b/modules/catalogs/src/common/dto/payment-terms/shared/payment-term-summary.dto.ts @@ -0,0 +1,15 @@ +import { z } from "zod/v4"; + +import { PaymentTermDueRuleSchema } from "./payment-term-due-rule.dto"; + +export const PaymentTermSummarySchema = z.object({ + id: z.uuid(), + company_id: z.uuid(), + + name: z.string(), + is_active: z.boolean(), + is_system: z.boolean(), + due_rules: z.array(PaymentTermDueRuleSchema), +}); + +export type PaymentTermSummaryDTO = z.infer;