diff --git a/modules/catalogs/src/api/application/index.ts b/modules/catalogs/src/api/application/index.ts index 7a4fc37c..23f8279d 100644 --- a/modules/catalogs/src/api/application/index.ts +++ b/modules/catalogs/src/api/application/index.ts @@ -1,2 +1,3 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-regimes"; diff --git a/modules/catalogs/src/api/application/payment-terms/services/index.ts b/modules/catalogs/src/api/application/payment-terms/services/index.ts index d832b626..135f0453 100644 --- a/modules/catalogs/src/api/application/payment-terms/services/index.ts +++ b/modules/catalogs/src/api/application/payment-terms/services/index.ts @@ -1,6 +1,6 @@ -export * from "./payment-term-creator"; -export * from "./payment-term-deleter"; -export * from "./payment-term-finder"; +export * from "./payment-term-creator.service"; +export * from "./payment-term-deleter.service"; +export * from "./payment-term-finder.service"; export * from "./payment-term-public-services"; -export * from "./payment-term-status-changer"; -export * from "./payment-term-updater"; +export * from "./payment-term-status-changer.service"; +export * from "./payment-term-updater.service"; 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.service.ts similarity index 100% rename from modules/catalogs/src/api/application/payment-terms/services/payment-term-creator.ts rename to modules/catalogs/src/api/application/payment-terms/services/payment-term-creator.service.ts 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.service.ts similarity index 100% rename from modules/catalogs/src/api/application/payment-terms/services/payment-term-deleter.ts rename to modules/catalogs/src/api/application/payment-terms/services/payment-term-deleter.service.ts 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.service.ts similarity index 100% rename from modules/catalogs/src/api/application/payment-terms/services/payment-term-finder.ts rename to modules/catalogs/src/api/application/payment-terms/services/payment-term-finder.service.ts 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 index b90e4fae..b4306fe9 100644 --- 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 @@ -1,11 +1,27 @@ -import type { IPaymentTermFinder } from "./payment-term-finder"; +import type { IPaymentTermCreator } from "./payment-term-creator.service"; +import type { IPaymentTermDeleter } from "./payment-term-deleter.service"; +import type { IPaymentTermFinder } from "./payment-term-finder.service"; +import type { IPaymentTermStatusChanger } from "./payment-term-status-changer.service"; +import type { IPaymentTermUpdater } from "./payment-term-updater.service"; export interface IPaymentTermPublicServices { finder: IPaymentTermFinder; + creator: IPaymentTermCreator; + updater: IPaymentTermUpdater; + deleter: IPaymentTermDeleter; + statusChanger: IPaymentTermStatusChanger; } export const buildPaymentTermPublicServices = ( - finder: IPaymentTermFinder + finder: IPaymentTermFinder, + creator: IPaymentTermCreator, + updater: IPaymentTermUpdater, + deleter: IPaymentTermDeleter, + statusChanger: IPaymentTermStatusChanger ): IPaymentTermPublicServices => ({ finder, + creator, + updater, + deleter, + statusChanger, }); 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.service.ts similarity index 92% rename from modules/catalogs/src/api/application/payment-terms/services/payment-term-status-changer.ts rename to modules/catalogs/src/api/application/payment-terms/services/payment-term-status-changer.service.ts index cdb6d2a5..a68dc6fe 100644 --- 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.service.ts @@ -1,7 +1,7 @@ import { Result } from "@repo/rdx-utils"; import type { PaymentTerm } from "../../../domain"; -import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface"; +import type { IPaymentTermRepository } from "../repositories"; export type PaymentTermStatusChangeAction = "enable" | "disable"; 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.service.ts similarity index 100% rename from modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.ts rename to modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.service.ts diff --git a/modules/catalogs/src/api/application/tax-regimes/di/index.ts b/modules/catalogs/src/api/application/tax-regimes/di/index.ts new file mode 100644 index 00000000..34ce3c8c --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/di/index.ts @@ -0,0 +1,3 @@ +export * from "./tax-regime-input-mappers.di"; +export * from "./tax-regime-services.di"; +export * from "./tax-regime-snapshot-builders.di"; diff --git a/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-input-mappers.di.ts b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-input-mappers.di.ts new file mode 100644 index 00000000..1c269c1b --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-input-mappers.di.ts @@ -0,0 +1,20 @@ +import { + CreateTaxRegimeInputMapper, + type ICreateTaxRegimeInputMapper, + UpdateTaxRegimeInputMapper, +} from "../mappers"; + +export interface ITaxRegimeInputMappers { + createInputMapper: ICreateTaxRegimeInputMapper; + updateInputMapper: UpdateTaxRegimeInputMapper; +} + +export const buildTaxRegimeInputMappers = (): ITaxRegimeInputMappers => { + const createInputMapper = new CreateTaxRegimeInputMapper(); + const updateInputMapper = new UpdateTaxRegimeInputMapper(); + + return { + createInputMapper, + updateInputMapper, + }; +}; diff --git a/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-services.di.ts b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-services.di.ts new file mode 100644 index 00000000..7d70cac0 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-services.di.ts @@ -0,0 +1,53 @@ +import type { ITaxRegimeRepository } from "../repositories"; +import { + type ITaxRegimeCreator, + type ITaxRegimeDeleter, + type ITaxRegimeFinder, + type ITaxRegimeStatusChanger, + type ITaxRegimeUpdater, + TaxRegimeCreator, + TaxRegimeDeleter, + TaxRegimeFinder, + TaxRegimeStatusChanger, + TaxRegimeUpdater, +} from "../services"; + +export function buildTaxRegimeFinder(params: { + repository: ITaxRegimeRepository; +}): ITaxRegimeFinder { + const { repository } = params; + + return new TaxRegimeFinder(repository); +} + +export const buildTaxRegimeCreator = (params: { + repository: ITaxRegimeRepository; +}): ITaxRegimeCreator => { + const { repository } = params; + + return new TaxRegimeCreator(repository); +}; + +export const buildTaxRegimeDeleter = (params: { + repository: ITaxRegimeRepository; +}): ITaxRegimeDeleter => { + const { repository } = params; + + return new TaxRegimeDeleter(repository); +}; + +export const buildTaxRegimeStatusChanger = (params: { + repository: ITaxRegimeRepository; +}): ITaxRegimeStatusChanger => { + const { repository } = params; + + return new TaxRegimeStatusChanger(repository); +}; + +export const buildTaxRegimeUpdater = (params: { + repository: ITaxRegimeRepository; +}): ITaxRegimeUpdater => { + const { repository } = params; + + return new TaxRegimeUpdater(repository); +}; diff --git a/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-snapshot-builders.di.ts b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-snapshot-builders.di.ts new file mode 100644 index 00000000..ea2cfa39 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/di/tax-regime-snapshot-builders.di.ts @@ -0,0 +1,14 @@ +import { + TaxRegimeFullSnapshotBuilder, + TaxRegimeSummarySnapshotBuilder, +} from "../snapshot-builders"; + +export function buildTaxRegimeSnapshotBuilders() { + const fullSnapshotBuilder = new TaxRegimeFullSnapshotBuilder(); + const summarySnapshotBuilder = new TaxRegimeSummarySnapshotBuilder(); + + return { + full: fullSnapshotBuilder, + summary: summarySnapshotBuilder, + }; +} diff --git a/modules/catalogs/src/api/application/tax-regimes/index.ts b/modules/catalogs/src/api/application/tax-regimes/index.ts new file mode 100644 index 00000000..f9c2a070 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/index.ts @@ -0,0 +1,6 @@ +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/tax-regimes/mappers/create-tax-regime-input.mapper.ts b/modules/catalogs/src/api/application/tax-regimes/mappers/create-tax-regime-input.mapper.ts new file mode 100644 index 00000000..5f96cfe2 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/mappers/create-tax-regime-input.mapper.ts @@ -0,0 +1,62 @@ +import { + DomainError, + TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { CreateTaxRegimeRequestDTO } from "../../../../common"; +import { type ITaxRegimeCreateProps, TaxRegimeCode } from "../../../domain"; + +export interface ICreateTaxRegimeInputMapper { + map( + dto: CreateTaxRegimeRequestDTO, + params: { companyId: UniqueID } + ): Result<{ id: UniqueID; props: ITaxRegimeCreateProps }, Error>; +} + +export class CreateTaxRegimeInputMapper implements ICreateTaxRegimeInputMapper { + public map( + dto: CreateTaxRegimeRequestDTO, + params: { companyId: UniqueID } + ): Result<{ id: UniqueID; props: ITaxRegimeCreateProps }, Error> { + const errors: ValidationErrorDetail[] = []; + + try { + const taxRegimeId = extractOrPushError(UniqueID.create(dto.id), "id", errors); + + const code = extractOrPushError(TaxRegimeCode.create(dto.code), "code", errors); + + const description = extractOrPushError( + TextValue.create(dto.description), + "description", + errors + ); + + const isActive = extractOrPushError(Result.ok(dto.is_active), "is_active", errors); + + this.throwIfValidationErrors(errors); + + const props: ITaxRegimeCreateProps = { + companyId: params.companyId, + code: code!, + description: description!, + isActive: isActive ?? true, + isSystem: false, + }; + + return Result.ok({ id: taxRegimeId!, props }); + } catch (err: unknown) { + return Result.fail(new DomainError("Tax regime props mapping failed", { cause: err })); + } + } + + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Tax regime props mapping failed", errors); + } + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/mappers/index.ts b/modules/catalogs/src/api/application/tax-regimes/mappers/index.ts new file mode 100644 index 00000000..57b05704 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./create-tax-regime-input.mapper"; +export * from "./update-tax-regime-by-id-input.mapper"; diff --git a/modules/catalogs/src/api/application/tax-regimes/mappers/update-tax-regime-by-id-input.mapper.ts b/modules/catalogs/src/api/application/tax-regimes/mappers/update-tax-regime-by-id-input.mapper.ts new file mode 100644 index 00000000..6be00389 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/mappers/update-tax-regime-by-id-input.mapper.ts @@ -0,0 +1,63 @@ +import { + DomainError, + TextValue, + type UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; + +import type { UpdateTaxRegimeByIdRequestDTO } from "../../../../common"; +import type { TaxRegimePatchProps } from "../../../domain"; + +export interface IUpdateTaxRegimeInputMapper { + map( + dto: UpdateTaxRegimeByIdRequestDTO, + _params: { companyId: UniqueID } + ): Result; +} + +export class UpdateTaxRegimeInputMapper implements IUpdateTaxRegimeInputMapper { + public map( + dto: UpdateTaxRegimeByIdRequestDTO, + _params: { companyId: UniqueID } + ): Result { + try { + const errors: ValidationErrorDetail[] = []; + const taxRegimePatchProps: TaxRegimePatchProps = {}; + + toPatchField(dto.description).ifSet((description) => { + if (isNullishOrEmpty(description)) { + errors.push({ path: "description", message: "Description cannot be empty" }); + return; + } + + taxRegimePatchProps.description = extractOrPushError( + TextValue.create(description), + "description", + errors + ); + }); + + toPatchField(dto.is_active).ifSet((isActive) => { + taxRegimePatchProps.isActive = isActive; + }); + + this.throwIfValidationErrors(errors); + + return Result.ok(taxRegimePatchProps); + } catch (err: unknown) { + console.error(err); + return Result.fail( + new DomainError("Tax regime props mapping failed (update)", { cause: err }) + ); + } + } + + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Tax regime props mapping failed", errors); + } + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/models/index.ts b/modules/catalogs/src/api/application/tax-regimes/models/index.ts new file mode 100644 index 00000000..c8857c12 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/models/index.ts @@ -0,0 +1,2 @@ +export * from "./tax-regime-detail.model"; +export * from "./tax-regime-summary.model"; diff --git a/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-detail.model.ts b/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-detail.model.ts new file mode 100644 index 00000000..ccd2e7e9 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-detail.model.ts @@ -0,0 +1,11 @@ +import type { TaxRegimeCode } from "@erp/catalogs/api/domain"; +import type { TextValue, UniqueID } from "@repo/rdx-ddd"; + +export type TaxRegimeDetail = { + id: UniqueID; + companyId: UniqueID; + code: TaxRegimeCode; + description: TextValue; + is_active: boolean; + is_system: boolean; +}; diff --git a/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-summary.model.ts b/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-summary.model.ts new file mode 100644 index 00000000..03892968 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/models/tax-regime-summary.model.ts @@ -0,0 +1,11 @@ +import type { TaxRegimeCode } from "@erp/catalogs/api/domain"; +import type { TextValue, UniqueID } from "@repo/rdx-ddd"; + +export type TaxRegimeSummary = { + id: UniqueID; + companyId: UniqueID; + code: TaxRegimeCode; + description: TextValue; + isActive: boolean; + isSystem: boolean; +}; diff --git a/modules/catalogs/src/api/application/tax-regimes/repositories/index.ts b/modules/catalogs/src/api/application/tax-regimes/repositories/index.ts new file mode 100644 index 00000000..d6f842ea --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/repositories/index.ts @@ -0,0 +1 @@ +export * from "./tax-regime-repository.interface"; diff --git a/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts b/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts new file mode 100644 index 00000000..21253aec --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/repositories/tax-regime-repository.interface.ts @@ -0,0 +1,40 @@ +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 { TaxRegime, TaxRegimeCode } from "../../../domain"; +import type { TaxRegimeSummary } from "../models"; + +export interface ITaxRegimeRepository { + create(taxRegime: TaxRegime, transaction?: unknown): Promise>; + update(taxRegime: TaxRegime, 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>; + + getByCodeInCompany( + companyId: UniqueID, + code: TaxRegimeCode, + transaction?: unknown + ): Promise>; + + findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>>; +} diff --git a/modules/catalogs/src/api/application/tax-regimes/services/index.ts b/modules/catalogs/src/api/application/tax-regimes/services/index.ts new file mode 100644 index 00000000..cf02c5ed --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/index.ts @@ -0,0 +1,6 @@ +export * from "./tax-regime-creator.service"; +export * from "./tax-regime-deleter.service"; +export * from "./tax-regime-finder.service"; +export * from "./tax-regime-public-services"; +export * from "./tax-regime-status-changer.service"; +export * from "./tax-regime-updater.service"; diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-creator.service.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-creator.service.ts new file mode 100644 index 00000000..ae3b1095 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-creator.service.ts @@ -0,0 +1,39 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { type ITaxRegimeCreateProps, TaxRegime } from "../../../domain"; +import type { ITaxRegimeRepository } from "../repositories"; + +export interface ITaxRegimeCreatorParams { + companyId: UniqueID; + id: UniqueID; + props: ITaxRegimeCreateProps; + transaction: unknown; +} + +export interface ITaxRegimeCreator { + create(params: ITaxRegimeCreatorParams): Promise>; +} + +export class TaxRegimeCreator implements ITaxRegimeCreator { + constructor(private readonly repository: ITaxRegimeRepository) {} + + public async create(params: ITaxRegimeCreatorParams): Promise> { + const { companyId, id, props, transaction } = params; + + // Create aggregate + const taxRegimeResult = TaxRegime.create({ ...props, companyId }, id); + + if (taxRegimeResult.isFailure) { + return taxRegimeResult; + } + + const taxRegime = taxRegimeResult.data; + const saveResult = await this.repository.create(taxRegime, transaction); + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(taxRegime); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-deleter.service.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-deleter.service.ts new file mode 100644 index 00000000..5e963299 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-deleter.service.ts @@ -0,0 +1,38 @@ +import { Result } from "@repo/rdx-utils"; + +import { type TaxRegime, TaxRegimeCannotBeDeletedError } from "../../../domain"; +import type { ITaxRegimeRepository } from "../repositories"; + +export interface ITaxRegimeDeleter { + delete(params: { + taxRegime: TaxRegime; + transaction?: unknown; + }): Promise>; +} + +export class TaxRegimeDeleter implements ITaxRegimeDeleter { + public constructor(private readonly repository: ITaxRegimeRepository) {} + + public async delete(params: { + taxRegime: TaxRegime; + transaction?: unknown; + }): Promise> { + const { taxRegime, transaction } = params; + + if (taxRegime.isSystem) { + return Result.fail(new TaxRegimeCannotBeDeletedError("System tax regime cannot be deleted.")); + } + + const deleteResult = await this.repository.deleteByIdInCompany( + taxRegime.companyId, + taxRegime.id, + transaction + ); + + if (deleteResult.isFailure) { + return Result.fail(deleteResult.error); + } + + return Result.ok(taxRegime); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-finder.service.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-finder.service.ts new file mode 100644 index 00000000..25625201 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-finder.service.ts @@ -0,0 +1,69 @@ +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 { TaxRegime, TaxRegimeCode } from "../../../domain"; +import type { TaxRegimeSummary } from "../models"; +import type { ITaxRegimeRepository } from "../repositories"; + +export interface ITaxRegimeFinder { + findTaxRegimeById( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; + + findTaxRegimeByCode( + companyId: UniqueID, + code: TaxRegimeCode, + transaction?: unknown + ): Promise>; + + taxRegimeExists( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; + + findTaxRegimesByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>>; +} + +export class TaxRegimeFinder implements ITaxRegimeFinder { + constructor(private readonly repository: ITaxRegimeRepository) {} + + async findTaxRegimeById( + companyId: UniqueID, + taxRegimeId: UniqueID, + transaction?: unknown + ): Promise> { + return this.repository.getByIdInCompany(companyId, taxRegimeId, transaction); + } + + async findTaxRegimeByCode( + companyId: UniqueID, + code: TaxRegimeCode, + transaction?: unknown + ): Promise> { + return this.repository.getByCodeInCompany(companyId, code, transaction); + } + + async taxRegimeExists( + companyId: UniqueID, + taxRegimeId: UniqueID, + transaction?: unknown + ): Promise> { + return this.repository.existsByIdInCompany(companyId, taxRegimeId, transaction); + } + + async findTaxRegimesByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>> { + return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-public-services.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-public-services.ts new file mode 100644 index 00000000..5ddceb98 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-public-services.ts @@ -0,0 +1,27 @@ +import type { ITaxRegimeCreator } from "./tax-regime-creator.service"; +import type { ITaxRegimeDeleter } from "./tax-regime-deleter.service"; +import type { ITaxRegimeFinder } from "./tax-regime-finder.service"; +import type { ITaxRegimeStatusChanger } from "./tax-regime-status-changer.service"; +import type { ITaxRegimeUpdater } from "./tax-regime-updater.service"; + +export interface ITaxRegimePublicServices { + finder: ITaxRegimeFinder; + creator: ITaxRegimeCreator; + updater: ITaxRegimeUpdater; + deleter: ITaxRegimeDeleter; + statusChanger: ITaxRegimeStatusChanger; +} + +export const buildTaxRegimePublicServices = ( + finder: ITaxRegimeFinder, + creator: ITaxRegimeCreator, + updater: ITaxRegimeUpdater, + deleter: ITaxRegimeDeleter, + statusChanger: ITaxRegimeStatusChanger +): ITaxRegimePublicServices => ({ + finder, + creator, + updater, + deleter, + statusChanger, +}); diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-status-changer.service.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-status-changer.service.ts new file mode 100644 index 00000000..440597bc --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-status-changer.service.ts @@ -0,0 +1,42 @@ +import type { TaxRegime } from "@erp/catalogs/api/domain"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxRegimeRepository } from "../repositories"; + +export type TaxRegimeStatusChangeAction = "enable" | "disable"; + +export interface ITaxRegimeStatusChanger { + changeStatus(params: { + taxRegime: TaxRegime; + action: TaxRegimeStatusChangeAction; + transaction?: unknown; + }): Promise>; +} + +export class TaxRegimeStatusChanger implements ITaxRegimeStatusChanger { + public constructor(private readonly repository: ITaxRegimeRepository) {} + + public async changeStatus(params: { + taxRegime: TaxRegime; + action: TaxRegimeStatusChangeAction; + transaction?: unknown; + }): Promise> { + const { taxRegime, action, transaction } = params; + + const statusResult = action === "enable" ? taxRegime.enable() : taxRegime.disable(); + if (statusResult.isFailure) { + return Result.fail(statusResult.error); + } + + if (!statusResult.data) { + return Result.ok(taxRegime); + } + + const persistenceResult = await this.repository.update(taxRegime, transaction); + if (persistenceResult.isFailure) { + return Result.fail(persistenceResult.error); + } + + return Result.ok(taxRegime); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-updater.service.ts b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-updater.service.ts new file mode 100644 index 00000000..1cee4079 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/services/tax-regime-updater.service.ts @@ -0,0 +1,52 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { TaxRegime, TaxRegimePatchProps } from "../../../domain"; +import type { ITaxRegimeRepository } from "../repositories"; + +export interface ITaxRegimeUpdater { + update(params: { + companyId: UniqueID; + id: UniqueID; + patchProps: TaxRegimePatchProps; + transaction?: unknown; + }): Promise>; +} + +export class TaxRegimeUpdater implements ITaxRegimeUpdater { + constructor(private readonly repository: ITaxRegimeRepository) {} + + public async update(params: { + companyId: UniqueID; + id: UniqueID; + patchProps: TaxRegimePatchProps; + 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 taxRegime = existingResult.data; + + // Aplicar cambios en el agregado + const updateResult = taxRegime.update(patchProps); + if (updateResult.isFailure) { + return Result.fail(updateResult.error); + } + + if (!updateResult.data) { + return Result.ok(taxRegime); + } + + // Persistir cambios + const saveResult = await this.repository.update(taxRegime, transaction); + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(taxRegime); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/full-tax-regime-snapshot.builder.ts b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/full-tax-regime-snapshot.builder.ts new file mode 100644 index 00000000..360b14a1 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/full-tax-regime-snapshot.builder.ts @@ -0,0 +1,20 @@ +import type { GetTaxRegimeByIdResponseDTO } from "@erp/catalogs/common"; +import type { ISnapshotBuilder } from "@erp/core/api"; + +import type { TaxRegime } from "../../../../domain"; + +export interface ITaxRegimeFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class TaxRegimeFullSnapshotBuilder implements ITaxRegimeFullSnapshotBuilder { + public toOutput(taxRegime: TaxRegime): GetTaxRegimeByIdResponseDTO { + return { + id: taxRegime.code.toPrimitive(), + company_id: taxRegime.companyId.toPrimitive(), + code: taxRegime.code.toPrimitive(), + description: taxRegime.description.toPrimitive(), + is_active: taxRegime.isActive, + is_system: taxRegime.isSystem, + }; + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/index.ts b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/index.ts new file mode 100644 index 00000000..847a7185 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/full/index.ts @@ -0,0 +1 @@ +export * from "./full-tax-regime-snapshot.builder"; diff --git a/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/index.ts b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/index.ts new file mode 100644 index 00000000..3b83e1ff --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/index.ts @@ -0,0 +1,2 @@ +export * from "./full"; +export * from "./summary"; diff --git a/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/index.ts b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/index.ts new file mode 100644 index 00000000..35c32412 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/index.ts @@ -0,0 +1 @@ +export * from "./summary-tax-regime-snapshot.builder"; diff --git a/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/summary-tax-regime-snapshot.builder.ts b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/summary-tax-regime-snapshot.builder.ts new file mode 100644 index 00000000..85ed1bf9 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/snapshot-builders/summary/summary-tax-regime-snapshot.builder.ts @@ -0,0 +1,20 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; + +import type { TaxRegimeSummaryDTO } from "../../../../../common"; +import type { TaxRegimeSummary } from "../../models"; + +export interface ITaxRegimeSummarySnapshotBuilder + extends ISnapshotBuilder {} + +export class TaxRegimeSummarySnapshotBuilder implements ITaxRegimeSummarySnapshotBuilder { + public toOutput(taxRegime: TaxRegimeSummary): TaxRegimeSummaryDTO { + return { + id: taxRegime.id.toString(), + company_id: taxRegime.companyId.toString(), + code: taxRegime.code.toPrimitive(), + description: taxRegime.description.toPrimitive(), + is_active: taxRegime.isActive, + is_system: taxRegime.isSystem, + }; + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/use-cases/create-tax-regime.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/create-tax-regime.use-case.ts new file mode 100644 index 00000000..2692bbe8 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/create-tax-regime.use-case.ts @@ -0,0 +1,55 @@ +import type { CreateTaxRegimeRequestDTO } 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 { ICreateTaxRegimeInputMapper } from "../mappers"; +import type { ITaxRegimeCreator } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type CreateTaxRegimeUseCaseInput = { + companyId: UniqueID; + dto: CreateTaxRegimeRequestDTO; +}; + +type CreateTaxRegimeUseCaseDeps = { + dtoMapper: ICreateTaxRegimeInputMapper; + creator: ITaxRegimeCreator; + fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder; + transactionManager: ITransactionManager; +}; + +export class CreateTaxRegimeUseCase { + constructor(private readonly deps: CreateTaxRegimeUseCaseDeps) {} + + public execute(params: CreateTaxRegimeUseCaseInput) { + 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/tax-regimes/use-cases/delete-tax-regime-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/delete-tax-regime-by-id.use-case.ts new file mode 100644 index 00000000..5418ef7a --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/delete-tax-regime-by-id.use-case.ts @@ -0,0 +1,57 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxRegimeDeleter, ITaxRegimeFinder } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type DeleteTaxRegimeByIdUseCaseInput = { + companyId: UniqueID; + tax_regime_id: string; +}; + +export class DeleteTaxRegimeByIdUseCase { + constructor( + private readonly deps: { + finder: ITaxRegimeFinder; + deleter: ITaxRegimeDeleter; + fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: DeleteTaxRegimeByIdUseCaseInput) { + const { tax_regime_id, companyId } = params; + + const idOrError = UniqueID.create(tax_regime_id); + if (idOrError.isFailure) return Result.fail(idOrError.error); + + const taxRegimeId = idOrError.data; + + return this.deps.transactionManager.complete(async (transaction: unknown) => { + try { + const findResult = await this.deps.finder.findTaxRegimeById( + companyId, + taxRegimeId, + transaction + ); + if (findResult.isFailure) { + return Result.fail(findResult.error); + } + + const deleteResult = await this.deps.deleter.delete({ + taxRegime: 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/tax-regimes/use-cases/disable-tax-regime-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/disable-tax-regime-by-id.use-case.ts new file mode 100644 index 00000000..ebe0d4e5 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/disable-tax-regime-by-id.use-case.ts @@ -0,0 +1,58 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxRegimeFinder, ITaxRegimeStatusChanger } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type DisableTaxRegimeByIdUseCaseInput = { + companyId: UniqueID; + tax_regime_id: string; +}; + +export class DisableTaxRegimeByIdUseCase { + constructor( + private readonly deps: { + finder: ITaxRegimeFinder; + changer: ITaxRegimeStatusChanger; + fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: DisableTaxRegimeByIdUseCaseInput) { + const { tax_regime_id, companyId } = params; + + const idOrError = UniqueID.create(tax_regime_id); + if (idOrError.isFailure) return Result.fail(idOrError.error); + + const taxRegimeId = idOrError.data; + + return this.deps.transactionManager.complete(async (transaction: unknown) => { + try { + const findResult = await this.deps.finder.findTaxRegimeById( + companyId, + taxRegimeId, + transaction + ); + if (findResult.isFailure) { + return Result.fail(findResult.error); + } + + const disableResult = await this.deps.changer.changeStatus({ + taxRegime: 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/tax-regimes/use-cases/enable-tax-regime-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/enable-tax-regime-by-id.use-case.ts new file mode 100644 index 00000000..c64cf07c --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/enable-tax-regime-by-id.use-case.ts @@ -0,0 +1,58 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxRegimeFinder, ITaxRegimeStatusChanger } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type EnableTaxRegimeByIdUseCaseInput = { + companyId: UniqueID; + tax_regime_id: string; +}; + +export class EnableTaxRegimeByIdUseCase { + constructor( + private readonly deps: { + finder: ITaxRegimeFinder; + changer: ITaxRegimeStatusChanger; + fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: EnableTaxRegimeByIdUseCaseInput) { + const { tax_regime_id, companyId } = params; + + const idOrError = UniqueID.create(tax_regime_id); + if (idOrError.isFailure) return Result.fail(idOrError.error); + + const taxRegimeId = idOrError.data; + + return this.deps.transactionManager.complete(async (transaction: unknown) => { + try { + const findResult = await this.deps.finder.findTaxRegimeById( + companyId, + taxRegimeId, + transaction + ); + if (findResult.isFailure) { + return Result.fail(findResult.error); + } + + const enableResult = await this.deps.changer.changeStatus({ + taxRegime: 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/tax-regimes/use-cases/get-tax-regime-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/get-tax-regime-by-id.use-case.ts new file mode 100644 index 00000000..2105d548 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/get-tax-regime-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 { ITaxRegimeFinder } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type GetTaxRegimeByIdUseCaseInput = { + companyId: UniqueID; + tax_regime_id: string; +}; + +export class GetTaxRegimeByIdUseCase { + constructor( + private readonly finder: ITaxRegimeFinder, + private readonly fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: GetTaxRegimeByIdUseCaseInput) { + const { tax_regime_id, companyId } = params; + + const idOrError = UniqueID.create(tax_regime_id); + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const taxRegimeId = idOrError.data; + + return this.transactionManager.complete(async (transaction: unknown) => { + try { + const result = await this.finder.findTaxRegimeById(companyId, taxRegimeId, 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/tax-regimes/use-cases/index.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/index.ts new file mode 100644 index 00000000..a1f95889 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/index.ts @@ -0,0 +1,7 @@ +export * from "./create-tax-regime.use-case"; +export * from "./delete-tax-regime-by-id.use-case"; +export * from "./disable-tax-regime-by-id.use-case"; +export * from "./enable-tax-regime-by-id.use-case"; +export * from "./get-tax-regime-by-id.use-case"; +export * from "./list-tax-regimes.use-case"; +export * from "./update-tax-regime-by-id.use-case"; diff --git a/modules/catalogs/src/api/application/tax-regimes/use-cases/list-tax-regimes.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/list-tax-regimes.use-case.ts new file mode 100644 index 00000000..fd5aba3d --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/list-tax-regimes.use-case.ts @@ -0,0 +1,55 @@ +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 { ITaxRegimeFinder } from "../services"; +import type { ITaxRegimeSummarySnapshotBuilder } from "../snapshot-builders"; + +type ListTaxRegimesUseCaseInput = { + companyId: UniqueID; + criteria: Criteria; +}; + +export class ListTaxRegimesUseCase { + constructor( + private readonly finder: ITaxRegimeFinder, + private readonly summarySnapshotBuilder: ITaxRegimeSummarySnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: ListTaxRegimesUseCaseInput) { + const { criteria, companyId } = params; + + return this.transactionManager.complete(async (transaction: unknown) => { + try { + const result = await this.finder.findTaxRegimesByCriteria(companyId, criteria, transaction); + + if (result.isFailure) { + return Result.fail(result.error); + } + + const taxRegimes = result.data; + const totalItems = taxRegimes.total(); + + const items = taxRegimes.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: "tax_regimes", + criteria: criteria.toJSON(), + }, + }; + + return Result.ok(snapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/tax-regimes/use-cases/update-tax-regime-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-regimes/use-cases/update-tax-regime-by-id.use-case.ts new file mode 100644 index 00000000..b34c4922 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-regimes/use-cases/update-tax-regime-by-id.use-case.ts @@ -0,0 +1,63 @@ +import type { UpdateTaxRegimeByIdRequestDTO } 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 { IUpdateTaxRegimeInputMapper } from "../mappers"; +import type { ITaxRegimeFinder, ITaxRegimeUpdater } from "../services"; +import type { ITaxRegimeFullSnapshotBuilder } from "../snapshot-builders"; + +export type UpdateTaxRegimeByIdUseCaseInput = { + companyId: UniqueID; + tax_regime_id: string; + dto: UpdateTaxRegimeByIdRequestDTO; +}; + +export class UpdateTaxRegimeByIdUseCase { + constructor( + private readonly deps: { + updater: ITaxRegimeUpdater; + finder: ITaxRegimeFinder; + dtoMapper: IUpdateTaxRegimeInputMapper; + fullSnapshotBuilder: ITaxRegimeFullSnapshotBuilder; + transactionManager: ITransactionManager; + } + ) {} + + public execute(params: UpdateTaxRegimeByIdUseCaseInput) { + const { companyId, tax_regime_id, dto } = params; + + const idOrError = UniqueID.create(tax_regime_id); + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const taxRegimeId = 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: taxRegimeId, + 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 7a4fc37c..23f8279d 100644 --- a/modules/catalogs/src/api/domain/index.ts +++ b/modules/catalogs/src/api/domain/index.ts @@ -1,2 +1,3 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-regimes"; 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 index 0ba8eb3b..e56cd439 100644 --- a/modules/catalogs/src/api/domain/payment-terms/payment-term.aggregate.ts +++ b/modules/catalogs/src/api/domain/payment-terms/payment-term.aggregate.ts @@ -154,7 +154,7 @@ export class PaymentTerm extends AggregateRoot { return this._dueRules; } - public update(patchProps: PaymentTermPatchProps): Result { + public update(patchProps: PaymentTermPatchProps): Result { if (this.isSystem) { return Result.fail( new PaymentTermCannotBeUpdatedError("System payment terms cannot be updated.") @@ -166,6 +166,7 @@ export class PaymentTerm extends AggregateRoot { return Result.fail(validationResult.error); } + let hasChanges = false; const { dueRules, ...otherProps } = patchProps; const candidateProps: PaymentTermInternalProps = { @@ -175,6 +176,7 @@ export class PaymentTerm extends AggregateRoot { // Aplicar cambios Object.assign(this.props, candidateProps); + hasChanges = true; // Reemplazo de items (si se proporciona) if (patchProps.dueRules !== undefined) { @@ -184,7 +186,7 @@ export class PaymentTerm extends AggregateRoot { } } - return Result.ok(); + return Result.ok(hasChanges); } public disable(): Result { diff --git a/modules/catalogs/src/api/domain/tax-regimes/errors.ts b/modules/catalogs/src/api/domain/tax-regimes/errors.ts new file mode 100644 index 00000000..a4db1cbd --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-regimes/errors.ts @@ -0,0 +1,66 @@ +import { DomainError } from "@repo/rdx-ddd"; + +export class InvalidTaxRegimeIdError extends DomainError { + public readonly code = "TAX_REGIME_INVALID_ID" as const; +} + +export const isInvalidTaxRegimeIdError = (e: unknown): e is InvalidTaxRegimeIdError => + e instanceof InvalidTaxRegimeIdError; + +export class InvalidTaxRegimeCodeError extends DomainError { + public readonly code = "TAX_REGIME_INVALID_CODE" as const; +} + +export const isInvalidTaxRegimeCodeError = (e: unknown): e is InvalidTaxRegimeCodeError => + e instanceof InvalidTaxRegimeCodeError; + +// + +export class InvalidTaxRegimeDescriptionError extends DomainError { + public readonly code = "TAX_REGIME_DESCRIPTION" as const; +} + +export const isInvalidTaxRegimeDescriptionError = ( + e: unknown +): e is InvalidTaxRegimeDescriptionError => e instanceof InvalidTaxRegimeDescriptionError; + +// + +export class TaxRegimeNotFoundError extends DomainError { + public readonly code = "TAX_REGIME_NOT_FOUND" as const; +} + +export const isTaxRegimeNotFoundError = (e: unknown): e is TaxRegimeNotFoundError => + e instanceof TaxRegimeNotFoundError; + +// + +export class TaxRegimeCannotBeDeletedError extends DomainError { + public readonly code = "TAX_REGIME_CANNOT_BE_DELETED" as const; +} + +export const isTaxRegimeCannotBeDeletedError = (e: unknown): e is TaxRegimeCannotBeDeletedError => + e instanceof TaxRegimeCannotBeDeletedError; + +// + +export class TaxRegimeCannotBeUpdatedError extends DomainError { + public readonly code = "TAX_REGIME_CANNOT_BE_UPDATED" as const; +} + +export const isTaxRegimeCannotBeUpdatedError = (e: unknown): e is TaxRegimeCannotBeUpdatedError => + e instanceof TaxRegimeCannotBeUpdatedError; + +export class TaxRegimeCannotBeEnabledError extends DomainError { + public readonly code = "TAX_REGIME_CANNOT_BE_ENABLED" as const; +} + +export const isTaxRegimeCannotBeEnabledError = (e: unknown): e is TaxRegimeCannotBeEnabledError => + e instanceof TaxRegimeCannotBeEnabledError; + +export class TaxRegimeCannotBeDisabledError extends DomainError { + public readonly code = "TAX_REGIME_CANNOT_BE_DISABLED" as const; +} + +export const isTaxRegimeCannotBeDisabledError = (e: unknown): e is TaxRegimeCannotBeDisabledError => + e instanceof TaxRegimeCannotBeDisabledError; diff --git a/modules/catalogs/src/api/domain/tax-regimes/index.ts b/modules/catalogs/src/api/domain/tax-regimes/index.ts new file mode 100644 index 00000000..f0873bf5 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-regimes/index.ts @@ -0,0 +1,3 @@ +export * from "./errors"; +export * from "./tax-regime.aggregate"; +export * from "./tax-regime-code"; diff --git a/modules/catalogs/src/api/domain/tax-regimes/tax-regime-code.ts b/modules/catalogs/src/api/domain/tax-regimes/tax-regime-code.ts new file mode 100644 index 00000000..7dcd257b --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-regimes/tax-regime-code.ts @@ -0,0 +1,28 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxRegimeCodeError } from "./errors"; + +export class TaxRegimeCode { + private constructor(private readonly value: string) {} + + public static create(code: string): Result { + const trimmed = code?.trim() ?? ""; + if (trimmed.length === 0) { + return Result.fail(new InvalidTaxRegimeCodeError("Tax regime code cannot be empty")); + } + + return Result.ok(new TaxRegimeCode(trimmed)); + } + + public static fromPersistence(code: string): TaxRegimeCode { + return new TaxRegimeCode(code); + } + + public toString(): string { + return this.value; + } + + public toPrimitive(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-regimes/tax-regime.aggregate.ts b/modules/catalogs/src/api/domain/tax-regimes/tax-regime.aggregate.ts new file mode 100644 index 00000000..244cfd72 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-regimes/tax-regime.aggregate.ts @@ -0,0 +1,158 @@ +import { AggregateRoot, type TextValue, type UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { TaxRegimeCannotBeUpdatedError } from "./errors"; +import type { TaxRegimeCode } from "./tax-regime-code"; + +export interface ITaxRegimeCreateProps { + companyId: UniqueID; + code: TaxRegimeCode; + description: TextValue; + isActive: boolean; + isSystem: boolean; +} + +export type TaxRegimePatchProps = Partial<{ + description: TextValue; + isActive: boolean; +}>; + +export type TaxRegimeInternalProps = ITaxRegimeCreateProps; + +export class TaxRegime extends AggregateRoot { + protected constructor(props: TaxRegimeInternalProps, id?: UniqueID) { + super(props, id); // eslint-disable-line @typescript-eslint/no-unused-vars + } + + public static create(props: ITaxRegimeCreateProps, id?: UniqueID): Result { + const validationResult = TaxRegime.validateCreateProps(props); + + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + // Crear instancia + const taxRegime = new TaxRegime(props, id); + + // Disparar eventos de dominio + // ... + // ... + + return Result.ok(taxRegime); + } + + public static rehydrate(props: TaxRegimeInternalProps, id: UniqueID): TaxRegime { + return new TaxRegime(props, id); + } + + private static validateCreateProps(props: ITaxRegimeCreateProps): Result { + if (!props.companyId) { + return Result.fail(new Error("Payment term company ID is required")); + } + if (!props.code) { + return Result.fail(new Error("Tax regime code is required")); + } + + return Result.ok(); + } + + private static validatePatchProps(patchProps: TaxRegimePatchProps): Result { + if (Object.keys(patchProps).length === 0) { + return Result.ok(); + } + + return Result.ok(); + } + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get code(): TaxRegimeCode { + return this.props.code; + } + + public get description(): TextValue { + return this.props.description; + } + + public get isActive(): boolean { + return this.props.isActive; + } + + public get isSystem(): boolean { + return this.props.isSystem; + } + + public update(patchProps: TaxRegimePatchProps): Result { + if (this.isSystem) { + return Result.fail( + new TaxRegimeCannotBeUpdatedError("System tax regimes cannot be updated.") + ); + } + + const validationResult = TaxRegime.validatePatchProps(patchProps); + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + + let hasChanges = false; + + if (patchProps.description !== undefined) { + if (!TaxRegime.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; + } + + return Result.ok(hasChanges); + } + + public disable(): Result { + if (this.isSystem) { + return Result.fail( + new TaxRegimeCannotBeUpdatedError("System tax regimes 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); + } + + public toJSON() { + return { + id: this.id.toPrimitive(), + code: this.code.toPrimitive(), + description: this.description.toPrimitive(), + is_active: this.isActive, + is_system: this.isSystem, + }; + } + + private static sameDescription(current: TextValue, next: TextValue): boolean { + return current.toPrimitive() === next.toPrimitive(); + } +} diff --git a/modules/catalogs/src/api/index.ts b/modules/catalogs/src/api/index.ts index 23bc077b..d0116c1b 100644 --- a/modules/catalogs/src/api/index.ts +++ b/modules/catalogs/src/api/index.ts @@ -5,6 +5,8 @@ import { paymentMethodsRouter, paymentTermModels, paymentTermsRouter, + taxRegimeModels, + taxRegimesRouter, } from "./infrastructure"; import { buildCatalogsDependencies, @@ -27,10 +29,11 @@ export const catalogsAPIModule: IModuleServer = { }); return { - models: [...paymentMethodModels, ...paymentTermModels], + models: [...paymentMethodModels, ...paymentTermModels, ...taxRegimeModels], services: { paymentMethod: publicServices.paymentMethods, paymentTerms: publicServices.paymentTerms, + taxRegimes: publicServices.taxRegimes, }, internal, }; @@ -41,6 +44,7 @@ export const catalogsAPIModule: IModuleServer = { paymentMethodsRouter(params); paymentTermsRouter(params); + taxRegimesRouter(params); logger.info("馃殌 Catalogs module started", { label: this.name, diff --git a/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts index 39b10ca6..ebf601e4 100644 --- a/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts +++ b/modules/catalogs/src/api/infrastructure/di/catalogs.di.ts @@ -10,16 +10,23 @@ import { buildPaymentTermsDependencies, buildPaymentTermsPublicServices, } from "../payment-terms/di"; +import { + type TaxRegimesInternalDeps, + buildTaxRegimesDependencies, + buildTaxRegimesPublicServices, +} from "../tax-regimes/di"; export type CatalogsInternalDeps = { paymentMethods: PaymentMethodsInternalDeps; paymentTerms: PaymentTermsInternalDeps; + taxRegimes: TaxRegimesInternalDeps; }; export const buildCatalogsDependencies = (params: ModuleParams): CatalogsInternalDeps => { return { paymentMethods: buildPaymentMethodsDependencies(params), paymentTerms: buildPaymentTermsDependencies(params), + taxRegimes: buildTaxRegimesDependencies(params), }; }; @@ -27,5 +34,6 @@ export const buildCatalogsPublicServices = (params: SetupParams, deps: CatalogsI return { paymentMethods: buildPaymentMethodsPublicServices(params, deps.paymentMethods), paymentTerms: buildPaymentTermsPublicServices(params, deps.paymentTerms), + taxRegimes: buildTaxRegimesPublicServices(params, deps.taxRegimes), }; }; diff --git a/modules/catalogs/src/api/infrastructure/index.ts b/modules/catalogs/src/api/infrastructure/index.ts index 7a4fc37c..23f8279d 100644 --- a/modules/catalogs/src/api/infrastructure/index.ts +++ b/modules/catalogs/src/api/infrastructure/index.ts @@ -1,2 +1,3 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-regimes"; diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-method-persistence-mappers.di.ts b/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-method-persistence-mappers.di.ts index 03499699..c453f734 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-method-persistence-mappers.di.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-method-persistence-mappers.di.ts @@ -1,13 +1,11 @@ import { SequelizePaymentMethodDomainMapper, SequelizePaymentMethodSummaryMapper, -} from "../persistence/index"; +} from "../persistence"; export interface IPaymentMethodPersistenceMappers { domainMapper: SequelizePaymentMethodDomainMapper; listMapper: SequelizePaymentMethodSummaryMapper; - - //createMapper: CreatePaymentMethodRequestMapper; } export const buildPaymentMethodPersistenceMappers = (): IPaymentMethodPersistenceMappers => { @@ -15,13 +13,8 @@ export const buildPaymentMethodPersistenceMappers = (): IPaymentMethodPersistenc const domainMapper = new SequelizePaymentMethodDomainMapper(); const listMapper = new SequelizePaymentMethodSummaryMapper(); - // Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado - //const createMapper = new CreatePaymentMethodRequestMapper(catalogs); - return { domainMapper, listMapper, - - //createMapper, }; }; 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 39abfe6c..5b5a1576 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 @@ -173,8 +173,6 @@ export class SequelizePaymentMethodRepository strictMode: true, // fuerza error si ORDER BY no permitido }); - console.log(query?.where); - query.where = { ...query.where, company_id: companyId.toString(), diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/di/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/di/index.ts new file mode 100644 index 00000000..443c4a97 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/di/index.ts @@ -0,0 +1,3 @@ +export * from "./tax-regime-persistence-mappers.di"; +export * from "./tax-regime-repositories.di"; +export * from "./tax-regimes.di"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-persistence-mappers.di.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-persistence-mappers.di.ts new file mode 100644 index 00000000..397d40b1 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-persistence-mappers.di.ts @@ -0,0 +1,17 @@ +import { SequelizeTaxRegimeDomainMapper, SequelizeTaxRegimeSummaryMapper } from "../persistence"; + +export interface ITaxRegimePersistenceMappers { + domainMapper: SequelizeTaxRegimeDomainMapper; + listMapper: SequelizeTaxRegimeSummaryMapper; +} + +export const buildTaxRegimePersistenceMappers = (): ITaxRegimePersistenceMappers => { + // Mappers para el repositorio + const domainMapper = new SequelizeTaxRegimeDomainMapper(); + const listMapper = new SequelizeTaxRegimeSummaryMapper(); + + return { + domainMapper, + listMapper, + }; +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-repositories.di.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-repositories.di.ts new file mode 100644 index 00000000..5afcf86c --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regime-repositories.di.ts @@ -0,0 +1,13 @@ +import type { Sequelize } from "sequelize"; + +import { SequelizeTaxRegimeRepository } from "../persistence"; + +import type { ITaxRegimePersistenceMappers } from "./tax-regime-persistence-mappers.di"; + +export const buildTaxRegimeRepository = (params: { + database: Sequelize; + mappers: ITaxRegimePersistenceMappers; +}) => { + const { database, mappers } = params; + return new SequelizeTaxRegimeRepository(mappers.domainMapper, mappers.listMapper, database); +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts new file mode 100644 index 00000000..7e110bff --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/di/tax-regimes.di.ts @@ -0,0 +1,119 @@ +import type { ModuleParams, SetupParams } from "@erp/core/api"; +import { buildTransactionManager } from "@erp/core/api"; +import type { Sequelize } from "sequelize"; + +import type { ITaxRegimeRepository } from "../../../application/"; +import { TaxRegimeFinder } from "../../../application/"; +import { + CreateTaxRegimeUseCase, + DeleteTaxRegimeByIdUseCase, + DisableTaxRegimeByIdUseCase, + EnableTaxRegimeByIdUseCase, + GetTaxRegimeByIdUseCase, + ListTaxRegimesUseCase, + UpdateTaxRegimeByIdUseCase, +} from "../../../application/tax-regimes"; +import { + buildTaxRegimeCreator, + buildTaxRegimeDeleter, + buildTaxRegimeFinder, + buildTaxRegimeInputMappers, + buildTaxRegimeSnapshotBuilders, + buildTaxRegimeStatusChanger, + buildTaxRegimeUpdater, +} from "../../../application/tax-regimes/di"; + +import { buildTaxRegimePersistenceMappers } from "./tax-regime-persistence-mappers.di"; +import { buildTaxRegimeRepository } from "./tax-regime-repositories.di"; + +export type TaxRegimesInternalDeps = { + repository: ITaxRegimeRepository; + useCases: { + listTaxRegimes: () => ListTaxRegimesUseCase; + getTaxRegimeById: () => GetTaxRegimeByIdUseCase; + createTaxRegime: () => CreateTaxRegimeUseCase; + updateTaxRegimeById: () => UpdateTaxRegimeByIdUseCase; + deleteTaxRegimeById: () => DeleteTaxRegimeByIdUseCase; + disableTaxRegimeById: () => DisableTaxRegimeByIdUseCase; + enableTaxRegimeById: () => EnableTaxRegimeByIdUseCase; + }; +}; + +export const buildTaxRegimesDependencies = (params: ModuleParams): TaxRegimesInternalDeps => { + const { database } = params; + + const transactionManager = buildTransactionManager(database as Sequelize); + const persistenceMappers = buildTaxRegimePersistenceMappers(); + + const repository = buildTaxRegimeRepository({ database, mappers: persistenceMappers }); + + const inputMappers = buildTaxRegimeInputMappers(); + + const finder = buildTaxRegimeFinder({ repository }); + const creator = buildTaxRegimeCreator({ repository }); + const updater = buildTaxRegimeUpdater({ repository }); + const deleter = buildTaxRegimeDeleter({ repository }); + const statusChanger = buildTaxRegimeStatusChanger({ repository }); + + const snapshotBuilders = buildTaxRegimeSnapshotBuilders(); + return { + repository, + useCases: { + listTaxRegimes: () => + new ListTaxRegimesUseCase(finder, snapshotBuilders.summary, transactionManager), + + getTaxRegimeById: () => + new GetTaxRegimeByIdUseCase(finder, snapshotBuilders.full, transactionManager), + + createTaxRegime: () => + new CreateTaxRegimeUseCase({ + dtoMapper: inputMappers.createInputMapper, + creator, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + updateTaxRegimeById: () => + new UpdateTaxRegimeByIdUseCase({ + updater, + finder, + dtoMapper: inputMappers.updateInputMapper, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + deleteTaxRegimeById: () => + new DeleteTaxRegimeByIdUseCase({ + deleter, + finder, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + disableTaxRegimeById: () => + new DisableTaxRegimeByIdUseCase({ + finder, + changer: statusChanger, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + enableTaxRegimeById: () => + new EnableTaxRegimeByIdUseCase({ + finder, + changer: statusChanger, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + }, + }; +}; + +export const buildTaxRegimesPublicServices = ( + _params: SetupParams, + deps: TaxRegimesInternalDeps +): { finder: TaxRegimeFinder } => { + return { + finder: new TaxRegimeFinder(deps.repository), + }; +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/create-tax-regime.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/create-tax-regime.controller.ts new file mode 100644 index 00000000..94f5d198 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/create-tax-regime.controller.ts @@ -0,0 +1,39 @@ +import type { CreateTaxRegimeRequestDTO } from "@erp/catalogs/common"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { CreateTaxRegimeUseCase } from "../../../../application/tax-regimes"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class CreateTaxRegimeController extends ExpressController { + constructor(private readonly useCase: CreateTaxRegimeUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + 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 CreateTaxRegimeRequestDTO; + 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/tax-regimes/express/controllers/delete-tax-regime-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/delete-tax-regime-by-id.controller.ts new file mode 100644 index 00000000..aff945bd --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/delete-tax-regime-by-id.controller.ts @@ -0,0 +1,38 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { DeleteTaxRegimeByIdUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class DeleteTaxRegimeByIdController extends ExpressController { + constructor(private readonly useCase: DeleteTaxRegimeByIdUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { tax_regime_id } = this.req.params; + const result = await this.useCase.execute({ tax_regime_id, companyId }); + + return result.match( + (data: unknown) => this.ok(data), + (err: Error) => this.handleError(err) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/disable-tax-regime-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/disable-tax-regime-by-id.controller.ts new file mode 100644 index 00000000..3df46a0a --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/disable-tax-regime-by-id.controller.ts @@ -0,0 +1,38 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { DisableTaxRegimeByIdUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class DisableTaxRegimeByIdController extends ExpressController { + constructor(private readonly useCase: DisableTaxRegimeByIdUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { tax_regime_id } = this.req.params; + const result = await this.useCase.execute({ tax_regime_id, companyId }); + + return result.match( + (data: unknown) => this.ok(data), + (err: Error) => this.handleError(err) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/enable-tax-regime-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/enable-tax-regime-by-id.controller.ts new file mode 100644 index 00000000..75604edf --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/enable-tax-regime-by-id.controller.ts @@ -0,0 +1,38 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { EnableTaxRegimeByIdUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class EnableTaxRegimeByIdController extends ExpressController { + constructor(private readonly useCase: EnableTaxRegimeByIdUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { tax_regime_id } = this.req.params; + const result = await this.useCase.execute({ tax_regime_id, companyId }); + + return result.match( + (data: unknown) => this.ok(data), + (err: Error) => this.handleError(err) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/get-tax-regime-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/get-tax-regime-by-id.controller.ts new file mode 100644 index 00000000..def1b721 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/get-tax-regime-by-id.controller.ts @@ -0,0 +1,38 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { GetTaxRegimeByIdUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class GetTaxRegimeByIdController extends ExpressController { + constructor(private readonly useCase: GetTaxRegimeByIdUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { tax_regime_id } = this.req.params; + const result = await this.useCase.execute({ tax_regime_id, companyId }); + + return result.match( + (data: unknown) => this.ok(data), + (err: Error) => this.handleError(err) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/index.ts new file mode 100644 index 00000000..588e429d --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/index.ts @@ -0,0 +1,7 @@ +export * from "./create-tax-regime.controller"; +export * from "./delete-tax-regime-by-id.controller"; +export * from "./disable-tax-regime-by-id.controller"; +export * from "./enable-tax-regime-by-id.controller"; +export * from "./get-tax-regime-by-id.controller"; +export * from "./list-tax-regimes.controller"; +export * from "./update-tax-regime-by-id.controller"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/list-tax-regimes.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/list-tax-regimes.controller.ts new file mode 100644 index 00000000..6c223f5e --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/list-tax-regimes.controller.ts @@ -0,0 +1,54 @@ +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; +import { Criteria } from "@repo/rdx-criteria/server"; + +import type { ListTaxRegimesUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class ListTaxRegimesController extends ExpressController { + constructor(private readonly useCase: ListTaxRegimesUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + 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, "code", "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/tax-regimes/express/controllers/update-tax-regime-by-id.controller.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/update-tax-regime-by-id.controller.ts new file mode 100644 index 00000000..dcca5b59 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/controllers/update-tax-regime-by-id.controller.ts @@ -0,0 +1,44 @@ +import type { UpdateTaxRegimeByIdRequestDTO } from "@erp/catalogs/common"; +import { + ExpressController, + forbidQueryFieldGuard, + requireAuthenticatedGuard, + requireCompanyContextGuard, +} from "@erp/core/api"; + +import type { UpdateTaxRegimeByIdUseCase } from "../../../../application"; +import { taxRegimesApiErrorMapper } from "../tax-regime-api-error-mapper"; + +export class UpdateTaxRegimeByIdController extends ExpressController { + constructor(private readonly useCase: UpdateTaxRegimeByIdUseCase) { + super(); + + this.errorMapper = taxRegimesApiErrorMapper; + + this.registerGuards( + requireAuthenticatedGuard(), + requireCompanyContextGuard(), + forbidQueryFieldGuard("companyId") + ); + } + + protected async executeImpl() { + const companyId = this.getTenantId(); + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } + + const { tax_regime_id } = this.req.params; + if (!tax_regime_id) { + return this.invalidInputError("Payment term ID missing"); + } + + const dto = this.req.body as UpdateTaxRegimeByIdRequestDTO; + const result = await this.useCase.execute({ tax_regime_id, companyId, dto }); + + return result.match( + (data: unknown) => this.ok(data), + (err: Error) => this.handleError(err) + ); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/index.ts new file mode 100644 index 00000000..7a94ce9f --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./tax-regimes.routes"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regime-api-error-mapper.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regime-api-error-mapper.ts new file mode 100644 index 00000000..3da73c8e --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regime-api-error-mapper.ts @@ -0,0 +1,107 @@ +import { + ApiErrorMapper, + ConflictApiError, + type ErrorToApiRule, + NotFoundApiError, + ValidationApiError, +} from "@erp/core/api"; + +import { + type InvalidTaxRegimeCodeError, + type InvalidTaxRegimeDescriptionError, + type InvalidTaxRegimeIdError, + type TaxRegimeCannotBeDeletedError, + type TaxRegimeCannotBeDisabledError, + type TaxRegimeCannotBeEnabledError, + type TaxRegimeCannotBeUpdatedError, + type TaxRegimeNotFoundError, + isInvalidTaxRegimeCodeError, + isInvalidTaxRegimeDescriptionError, + isInvalidTaxRegimeIdError, + isTaxRegimeCannotBeDeletedError, + isTaxRegimeCannotBeDisabledError, + isTaxRegimeCannotBeEnabledError, + isTaxRegimeCannotBeUpdatedError, + isTaxRegimeNotFoundError, +} from "../../../domain/tax-regimes"; + +const invalidTaxRegimeIdRule: ErrorToApiRule = { + priority: 120, + matches: isInvalidTaxRegimeIdError, + build: (error) => + new ConflictApiError( + (error as InvalidTaxRegimeIdError).message || + "Tax regime with the provided id already exists." + ), +}; + +const invalidTaxRegimeCodeRule: ErrorToApiRule = { + priority: 120, + matches: isInvalidTaxRegimeCodeError, + build: (error) => + new ValidationApiError( + (error as InvalidTaxRegimeCodeError).message || "Tax regime code is invalid." + ), +}; + +const invalidTaxRegimeDescriptionRule: ErrorToApiRule = { + priority: 120, + matches: isInvalidTaxRegimeDescriptionError, + build: (error) => + new ValidationApiError( + (error as InvalidTaxRegimeDescriptionError).message || "Tax regime description is invalid." + ), +}; + +const taxRegimeNotFoundRule: ErrorToApiRule = { + priority: 120, + matches: isTaxRegimeNotFoundError, + build: (error) => + new NotFoundApiError((error as TaxRegimeNotFoundError).message || "Tax regime not found."), +}; + +const taxRegimeCannotBeDeletedRule: ErrorToApiRule = { + priority: 120, + matches: isTaxRegimeCannotBeDeletedError, + build: (error) => + new ValidationApiError( + (error as TaxRegimeCannotBeDeletedError).message || "Tax regime cannot be deleted." + ), +}; + +const taxRegimeCannotBeDisabledRule: ErrorToApiRule = { + priority: 120, + matches: isTaxRegimeCannotBeDisabledError, + build: (error) => + new ValidationApiError( + (error as TaxRegimeCannotBeDisabledError).message || "Tax regime cannot be disabled." + ), +}; + +const taxRegimeCannotBeEnabledRule: ErrorToApiRule = { + priority: 120, + matches: isTaxRegimeCannotBeEnabledError, + build: (error) => + new ValidationApiError( + (error as TaxRegimeCannotBeEnabledError).message || "Tax regime cannot be enabled." + ), +}; + +const taxRegimeCannotBeUpdatedRule: ErrorToApiRule = { + priority: 120, + matches: isTaxRegimeCannotBeUpdatedError, + build: (error) => + new ValidationApiError( + (error as TaxRegimeCannotBeUpdatedError).message || "Tax regime cannot be updated." + ), +}; + +export const taxRegimesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() + .register(invalidTaxRegimeIdRule) + .register(invalidTaxRegimeCodeRule) + .register(invalidTaxRegimeDescriptionRule) + .register(taxRegimeNotFoundRule) + .register(taxRegimeCannotBeDeletedRule) + .register(taxRegimeCannotBeDisabledRule) + .register(taxRegimeCannotBeEnabledRule) + .register(taxRegimeCannotBeUpdatedRule); diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regimes.routes.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regimes.routes.ts new file mode 100644 index 00000000..2d4d95ad --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/express/tax-regimes.routes.ts @@ -0,0 +1,102 @@ +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 { + CreateTaxRegimeRequestSchema, + DeleteTaxRegimeByIdRequestSchema, + GetTaxRegimeByIdRequestSchema, + ListTaxRegimesRequestSchema, + UpdateTaxRegimeByIdParamsRequestSchema, + UpdateTaxRegimeByIdRequestSchema, +} from "../../../../common"; +import type { CatalogsInternalDeps } from "../../di"; + +import { + CreateTaxRegimeController, + DeleteTaxRegimeByIdController, + DisableTaxRegimeByIdController, + EnableTaxRegimeByIdController, + GetTaxRegimeByIdController, + ListTaxRegimesController, + UpdateTaxRegimeByIdController, +} from "./controllers"; + +export const taxRegimesRouter = (params: StartParams) => { + const { app, config, getInternal } = params; + const deps = getInternal("catalogs").taxRegimes; + + 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(ListTaxRegimesRequestSchema, "query"), (req, res, next) => { + const controller = new ListTaxRegimesController(deps.useCases.listTaxRegimes()); + return controller.execute(req, res, next); + }); + + router.post("/", validateRequest(CreateTaxRegimeRequestSchema, "body"), (req, res, next) => { + const controller = new CreateTaxRegimeController(deps.useCases.createTaxRegime()); + return controller.execute(req, res, next); + }); + + router.get( + "/:tax_regime_id", + validateRequest(GetTaxRegimeByIdRequestSchema, "params"), + (req, res, next) => { + const controller = new GetTaxRegimeByIdController(deps.useCases.getTaxRegimeById()); + return controller.execute(req, res, next); + } + ); + + router.delete( + "/:tax_regime_id", + validateRequest(DeleteTaxRegimeByIdRequestSchema, "params"), + (req, res, next) => { + const controller = new DeleteTaxRegimeByIdController(deps.useCases.deleteTaxRegimeById()); + return controller.execute(req, res, next); + } + ); + + router.put( + "/:tax_regime_id", + validateRequest(UpdateTaxRegimeByIdParamsRequestSchema, "params"), + validateRequest(UpdateTaxRegimeByIdRequestSchema, "body"), + (req, res, next) => { + const controller = new UpdateTaxRegimeByIdController(deps.useCases.updateTaxRegimeById()); + return controller.execute(req, res, next); + } + ); + + router.patch( + "/:tax_regime_id/disable", + validateRequest(GetTaxRegimeByIdRequestSchema, "params"), + (req, res, next) => { + const controller = new DisableTaxRegimeByIdController(deps.useCases.disableTaxRegimeById()); + return controller.execute(req, res, next); + } + ); + + router.patch( + "/:tax_regime_id/enable", + validateRequest(GetTaxRegimeByIdRequestSchema, "params"), + (req, res, next) => { + const controller = new EnableTaxRegimeByIdController(deps.useCases.enableTaxRegimeById()); + return controller.execute(req, res, next); + } + ); + + app.use(`${config.server.apiBasePath}/catalogs/tax-regimes`, router); +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/index.ts new file mode 100644 index 00000000..4263e0ee --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/index.ts @@ -0,0 +1,2 @@ +export * from "./express"; +export * from "./persistence"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/index.ts new file mode 100644 index 00000000..89b91e38 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/index.ts @@ -0,0 +1,7 @@ +export * from "./mappers"; +export * from "./models"; +export * from "./repositories"; + +import taxRegimeModelInit from "./models/sequelize-tax-regime.model"; + +export const taxRegimeModels = [taxRegimeModelInit]; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..3440c730 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./sequelize-tax-regime-domain.mapper"; +export * from "./sequelize-tax-regime-summary.mapper"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-domain.mapper.ts new file mode 100644 index 00000000..2d2bfbfe --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-domain.mapper.ts @@ -0,0 +1,79 @@ +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { TaxRegime, TaxRegimeCode } from "../../../../../domain"; +import type { TaxRegimeCreationAttributes, TaxRegimeModel } from "../models"; + +export class SequelizeTaxRegimeDomainMapper extends SequelizeDomainMapper< + TaxRegimeModel, + TaxRegimeCreationAttributes, + TaxRegime +> { + public mapToDomain(source: TaxRegimeModel, _params?: MapperParamsType): Result { + try { + const errors: ValidationErrorDetail[] = []; + + const idResult = UniqueID.create(source.id, true); + if (idResult.isFailure) { + return Result.fail(idResult.error); + } + + const companyId = extractOrPushError( + UniqueID.create(source.company_id), + "company_id", + errors + ); + + const code = extractOrPushError(TaxRegimeCode.create(source.code), "code", errors); + + const description = extractOrPushError( + TextValue.create(source.description), + "description", + errors + ); + + // Si hubo errores de mapeo, devolvemos colecci贸n de validaci贸n + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Payment method props mapping failed", errors) + ); + } + + const taxRegime = TaxRegime.rehydrate( + { + companyId: companyId!, + code: code!, + description: description!, + isActive: source.is_active, + isSystem: source.is_system, + }, + idResult.data + ); + + return Result.ok(taxRegime); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + public mapToPersistence( + source: TaxRegime, + _params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toPrimitive(), + company_id: source.companyId.toPrimitive(), + code: source.code.toPrimitive(), + description: source.description.toPrimitive(), + is_active: source.isActive, + is_system: source.isSystem, + }); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-summary.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-summary.mapper.ts new file mode 100644 index 00000000..3e065b37 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/mappers/sequelize-tax-regime-summary.mapper.ts @@ -0,0 +1,53 @@ +import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { TaxRegimeSummary } from "../../../../../application"; +import { TaxRegimeCode } from "../../../../../domain"; +import type { TaxRegimeModel } from "../models"; + +export class SequelizeTaxRegimeSummaryMapper extends SequelizeQueryMapper< + TaxRegimeModel, + TaxRegimeSummary +> { + public mapToReadModel( + raw: TaxRegimeModel, + _params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + const id = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const code = extractOrPushError(TaxRegimeCode.create(raw.code), "code", errors); + const description = extractOrPushError( + TextValue.create(raw.description), + "description", + errors + ); + const isActive = raw.is_active; + const isSystem = raw.is_system; + + // Si hubo errores de mapeo, devolvemos colecci贸n de validaci贸n + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("TaxRegime mapping failed [mapToDTO]", errors) + ); + } + + return Result.ok({ + id: id!, + companyId: companyId!, + code: code!, + description: description!, + isActive: isActive, + isSystem: isSystem, + }); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/index.ts new file mode 100644 index 00000000..7d4ee127 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/index.ts @@ -0,0 +1 @@ +export { type TaxRegimeCreationAttributes, TaxRegimeModel } from "./sequelize-tax-regime.model"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts new file mode 100644 index 00000000..ac3875e4 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/models/sequelize-tax-regime.model.ts @@ -0,0 +1,89 @@ +import { + type CreationOptional, + DataTypes, + type InferAttributes, + type InferCreationAttributes, + Model, + type Sequelize, +} from "sequelize"; + +export type TaxRegimeCreationAttributes = InferCreationAttributes & {}; + +export class TaxRegimeModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: string; + declare company_id: string; + declare code: string; + declare description: CreationOptional; + declare is_active: boolean; + declare is_system: boolean; + + static associate(_database: Sequelize) {} + + static hooks(_database: Sequelize) {} +} + +export default (database: Sequelize) => { + TaxRegimeModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + company_id: { + type: DataTypes.UUID, + allowNull: false, + }, + code: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + }, + is_active: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + is_system: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }, + { + sequelize: database, + modelName: "TaxRegimeModel", + tableName: "tax_regimes", + + underscored: true, + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { + name: "idx_tax_regimes_code", + fields: ["code", "deleted_at"], + unique: true, + }, + ], + + whereMergeStrategy: "and", // <- c贸mo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + + return TaxRegimeModel; +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/index.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..92943646 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export { SequelizeTaxRegimeRepository } from "./sequelize-tax-regime.repository"; diff --git a/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts new file mode 100644 index 00000000..25be1046 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-regimes/persistence/sequelize/repositories/sequelize-tax-regime.repository.ts @@ -0,0 +1,253 @@ +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 { ITaxRegimeRepository, TaxRegimeSummary } from "../../../../../application"; +import type { TaxRegime, TaxRegimeCode } from "../../../../../domain"; +import type { SequelizeTaxRegimeDomainMapper, SequelizeTaxRegimeSummaryMapper } from "../mappers"; +import { TaxRegimeModel } from "../models"; + +export class SequelizeTaxRegimeRepository + extends SequelizeRepository + implements ITaxRegimeRepository +{ + constructor( + private readonly domainMapper: SequelizeTaxRegimeDomainMapper, + private readonly summaryMapper: SequelizeTaxRegimeSummaryMapper, + database: Sequelize + ) { + super({ database }); + } + + /** + * + * Crea un nuevo m茅todo de pago + * + * @param taxRegime - El m茅todo de pago nuevo a guardar. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + async create(taxRegime: TaxRegime, transaction?: Transaction): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(taxRegime); + if (dtoResult.isFailure) { + return Result.fail(dtoResult.error); + } + + await TaxRegimeModel.create(dtoResult.data, { transaction }); + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * Actualiza un m茅todo de pago existente. + * + * @param taxRegime - El m茅todo de pago a actualizar. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + async update(taxRegime: TaxRegime, transaction?: Transaction): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(taxRegime); + if (dtoResult.isFailure) { + return Result.fail(dtoResult.error); + } + + const { id, ...payload } = dtoResult.data; + const [affected] = await TaxRegimeModel.update(payload, { + where: { id }, + transaction, + individualHooks: true, + }); + + if (affected === 0) { + return Result.fail( + new InfrastructureRepositoryError("Concurrency conflict or payment method not found") + ); + } + + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * Comprueba si existe un TaxRegime con un `id` dentro de una `company`. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el m茅todo de pago. + * @param id - Identificador UUID del m茅todo de pago. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + async existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const count = await TaxRegimeModel.count({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(Boolean(count > 0)); + } catch (error: unknown) { + return Result.fail(translateSequelizeError(error)); + } + } + + /** + * Recupera un m茅todo de pago por su ID y companyId. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el m茅todo de pago. + * @param id - Identificador UUID del m茅todo de pago. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const row = await TaxRegimeModel.findOne({ + where: { + id: id.toString(), + company_id: companyId.toString(), + }, + transaction, + }); + if (!row) { + return Result.fail(new EntityNotFoundError("TaxRegime", "id", id.toString())); + } + + return this.domainMapper.mapToDomain(row); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * Recupera un m茅todo de pago por su code y companyId. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el m茅todo de pago. + * @param code - C贸digo del m茅todo de pago. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + + async getByCodeInCompany( + companyId: UniqueID, + code: TaxRegimeCode, + transaction?: Transaction + ): Promise> { + try { + const row = await TaxRegimeModel.findOne({ + where: { + code: code.toString(), + company_id: companyId.toString(), + }, + transaction, + }); + if (!row) { + return Result.fail(new EntityNotFoundError("TaxRegime", "code", code.toString())); + } + + return this.domainMapper.mapToDomain(row); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * Recupera m煤ltiples customers dentro de una empresa seg煤n un criterio din谩mico (b煤squeda, paginaci贸n, etc.). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param criteria - Criterios de b煤squeda. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result, Error> + * + * @see Criteria + */ + async findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + try { + const criteriaConverter = new CriteriaToSequelizeConverter(); + const query = criteriaConverter.convert(criteria, { + mappings: { + isActive: "is_active", + }, + searchableFields: [], + sortableFields: ["code"], + enableFullText: true, + database: this.database, + strictMode: true, // fuerza error si ORDER BY no permitido + }); + + query.where = { + ...query.where, + company_id: companyId.toString(), + deleted_at: null, + }; + + const [rows, count] = await Promise.all([ + TaxRegimeModel.findAll({ + ...query, + transaction, + }), + TaxRegimeModel.count({ + where: query.where, + distinct: true, // evita duplicados por LEFT JOIN + transaction, + }), + ]); + + return this.summaryMapper.mapToReadModelCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Elimina o marca como eliminado una forma de pago. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la forma de pago. + * @param id - UUID de la forma de pago a eliminar. + * @param transaction - Transacci贸n activa para la operaci贸n. + * @returns Result + */ + async deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction + ): Promise> { + try { + const deleted = await TaxRegimeModel.destroy({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + + if (deleted === 0) { + return Result.fail(new EntityNotFoundError("TaxRegime", "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 7a4fc37c..23f8279d 100644 --- a/modules/catalogs/src/common/dto/index.ts +++ b/modules/catalogs/src/common/dto/index.ts @@ -1,2 +1,3 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-regimes"; diff --git a/modules/catalogs/src/common/dto/tax-regimes/index.ts b/modules/catalogs/src/common/dto/tax-regimes/index.ts new file mode 100644 index 00000000..32497d2a --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/index.ts @@ -0,0 +1,3 @@ +export * from "./request"; +export * from "./response"; +export * from "./shared"; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/create-tax-regime.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/create-tax-regime.request.dto.ts new file mode 100644 index 00000000..51d6c545 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/create-tax-regime.request.dto.ts @@ -0,0 +1,10 @@ +import { z } from "zod/v4"; + +export const CreateTaxRegimeRequestSchema = z.object({ + id: z.uuid(), + code: z.string().regex(/^\d{2}$/), + description: z.string(), + is_active: z.boolean(), +}); + +export type CreateTaxRegimeRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/delete-tax-regime-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/delete-tax-regime-by-id.request.dto.ts new file mode 100644 index 00000000..d5953a6a --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/delete-tax-regime-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const DeleteTaxRegimeByIdRequestSchema = z.object({ + tax_regime_id: z.uuid(), +}); + +export type DeleteTaxRegimeByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/disable-tax-regime-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/disable-tax-regime-by-id.request.dto.ts new file mode 100644 index 00000000..5746611c --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/disable-tax-regime-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const DisableTaxRegimeByIdRequestSchema = z.object({ + tax_regime_id: z.uuid(), +}); + +export type DisableTaxRegimeByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/enable-tax-regime-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/enable-tax-regime-by-id.request.dto.ts new file mode 100644 index 00000000..67aafd13 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/enable-tax-regime-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const EnableTaxRegimeByIdRequestSchema = z.object({ + tax_regime_id: z.uuid(), +}); + +export type EnableTaxRegimeByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/get-tax-regime-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/get-tax-regime-by-id.request.dto.ts new file mode 100644 index 00000000..53231bca --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/get-tax-regime-by-id.request.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const GetTaxRegimeByIdRequestSchema = z.object({ + tax_regime_id: z.uuid(), +}); + +export type GetTaxRegimeByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/index.ts b/modules/catalogs/src/common/dto/tax-regimes/request/index.ts new file mode 100644 index 00000000..85e21364 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/index.ts @@ -0,0 +1,7 @@ +export * from "./create-tax-regime.request.dto"; +export * from "./delete-tax-regime-by-id.request.dto"; +export * from "./disable-tax-regime-by-id.request.dto"; +export * from "./enable-tax-regime-by-id.request.dto"; +export * from "./get-tax-regime-by-id.request.dto"; +export * from "./list-tax-regimes.request.dto"; +export * from "./update-tax-regime-by-id.request.dto"; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/list-tax-regimes.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/list-tax-regimes.request.dto.ts new file mode 100644 index 00000000..63e0dbcb --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/list-tax-regimes.request.dto.ts @@ -0,0 +1,5 @@ +import { CriteriaSchema } from "@erp/core"; +import type { z } from "zod/v4"; + +export const ListTaxRegimesRequestSchema = CriteriaSchema; +export type ListTaxRegimesRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/request/update-tax-regime-by-id.request.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/request/update-tax-regime-by-id.request.dto.ts new file mode 100644 index 00000000..6a79e329 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/request/update-tax-regime-by-id.request.dto.ts @@ -0,0 +1,15 @@ +import { z } from "zod/v4"; + +export const UpdateTaxRegimeByIdParamsRequestSchema = z.object({ + tax_regime_id: z.uuid(), +}); + +export const UpdateTaxRegimeByIdRequestSchema = z.object({ + description: z.string().optional(), + is_active: z.boolean().optional(), +}); + +export type UpdateTaxRegimeByIdParamsRequestDTO = z.infer< + typeof UpdateTaxRegimeByIdParamsRequestSchema +>; +export type UpdateTaxRegimeByIdRequestDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/create-tax-regime.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/create-tax-regime.response.dto.ts new file mode 100644 index 00000000..179badb9 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/create-tax-regime.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { TaxRegimeDetailSchema } from "../shared"; + +export const CreateTaxRegimeResponseSchema = TaxRegimeDetailSchema; +export type CreateTaxRegimeResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/disable-tax-regime-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/disable-tax-regime-by-id.response.dto.ts new file mode 100644 index 00000000..9e70c213 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/disable-tax-regime-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { TaxRegimeDetailSchema } from "../shared"; + +export const DisableTaxRegimeByIdResponseSchema = TaxRegimeDetailSchema; +export type DisableTaxRegimeByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/enable-tax-regime-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/enable-tax-regime-by-id.response.dto.ts new file mode 100644 index 00000000..e940fd2c --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/enable-tax-regime-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { TaxRegimeDetailSchema } from "../shared"; + +export const EnableTaxRegimeByIdResponseSchema = TaxRegimeDetailSchema; +export type EnableTaxRegimeByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/get-tax-regime-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/get-tax-regime-by-id.response.dto.ts new file mode 100644 index 00000000..2cb73f6b --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/get-tax-regime-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { TaxRegimeDetailSchema } from "../shared"; + +export const GetTaxRegimeByIdResponseSchema = TaxRegimeDetailSchema; +export type GetTaxRegimeByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/index.ts b/modules/catalogs/src/common/dto/tax-regimes/response/index.ts new file mode 100644 index 00000000..7d8725cb --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/index.ts @@ -0,0 +1,6 @@ +export * from "./create-tax-regime.response.dto"; +export * from "./disable-tax-regime-by-id.response.dto"; +export * from "./enable-tax-regime-by-id.response.dto"; +export * from "./get-tax-regime-by-id.response.dto"; +export * from "./list-tax-regimes.response.dto"; +export * from "./update-tax-regime-by-id.response.dto"; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/list-tax-regimes.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/list-tax-regimes.response.dto.ts new file mode 100644 index 00000000..03bf3103 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/list-tax-regimes.response.dto.ts @@ -0,0 +1,7 @@ +import { createPaginatedListSchema } from "@erp/core"; +import type { z } from "zod/v4"; + +import { TaxRegimeSummarySchema } from "../shared"; + +export const ListTaxRegimesResponseSchema = createPaginatedListSchema(TaxRegimeSummarySchema); +export type ListTaxRegimesResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/response/update-tax-regime-by-id.response.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/response/update-tax-regime-by-id.response.dto.ts new file mode 100644 index 00000000..7652465c --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/response/update-tax-regime-by-id.response.dto.ts @@ -0,0 +1,6 @@ +import type { z } from "zod/v4"; + +import { TaxRegimeDetailSchema } from "../shared"; + +export const UpdateTaxRegimeByIdResponseSchema = TaxRegimeDetailSchema; +export type UpdateTaxRegimeByIdResponseDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/shared/index.ts b/modules/catalogs/src/common/dto/tax-regimes/shared/index.ts new file mode 100644 index 00000000..104e5c4e --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/shared/index.ts @@ -0,0 +1,2 @@ +export * from "./tax-regime-detail.dto"; +export * from "./tax-regime-summary.dto"; diff --git a/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-detail.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-detail.dto.ts new file mode 100644 index 00000000..2b068f3a --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-detail.dto.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; + +export const TaxRegimeDetailSchema = z.object({ + id: z.uuid(), + company_id: z.uuid(), + code: z.string().regex(/^\d{2}$/), + description: z.string(), + is_active: z.boolean(), + is_system: z.boolean(), +}); + +export type TaxRegimeDetailDTO = z.infer; diff --git a/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-summary.dto.ts b/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-summary.dto.ts new file mode 100644 index 00000000..a5dd9b28 --- /dev/null +++ b/modules/catalogs/src/common/dto/tax-regimes/shared/tax-regime-summary.dto.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; + +export const TaxRegimeSummarySchema = z.object({ + id: z.uuid(), + company_id: z.uuid(), + code: z.string().regex(/^\d{2}$/), + description: z.string(), + is_active: z.boolean(), + is_system: z.boolean(), +}); + +export type TaxRegimeSummaryDTO = z.infer;