diff --git a/modules/catalogs/package.json b/modules/catalogs/package.json index 0013e5fc..07a003d9 100644 --- a/modules/catalogs/package.json +++ b/modules/catalogs/package.json @@ -16,8 +16,7 @@ "./common": "./src/common/index.ts", "./api": "./src/api/index.ts", "./client": "./src/web/manifest.ts", - "./client/payment-methods": "./src/web/payment-methods/index.ts", - "./client/payment-terms": "./src/web/payment-terms/index.ts" + "./client/*": "./src/web/*/index.ts" }, "peerDependencies": { "react": "^19.2.5", diff --git a/modules/catalogs/src/api/application/index.ts b/modules/catalogs/src/api/application/index.ts index 23f8279d..7b68c32a 100644 --- a/modules/catalogs/src/api/application/index.ts +++ b/modules/catalogs/src/api/application/index.ts @@ -1,3 +1,5 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./services"; +export * from "./tax-definitions"; export * from "./tax-regimes"; diff --git a/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts b/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts new file mode 100644 index 00000000..aac260c3 --- /dev/null +++ b/modules/catalogs/src/api/application/services/catalog-public-services.interface.ts @@ -0,0 +1,20 @@ +import type { UniqueID } from "@repo/rdx-ddd"; + +import type { IPaymentMethodPublicServices } from "../payment-methods"; +import type { IPaymentTermPublicServices } from "../payment-terms"; +import type { ITaxRegimePublicServices } from "../tax-regimes"; + +export type { IPaymentMethodPublicServices } from "../payment-methods"; +export type { IPaymentTermPublicServices } from "../payment-terms"; +export type { ITaxRegimePublicServices } from "../tax-regimes"; + +export interface ICatalogServicesContext { + transaction: unknown; + companyId: UniqueID; +} + +export interface ICatalogPublicServices { + paymentMethods: IPaymentMethodPublicServices; + paymentTerms: IPaymentTermPublicServices; + taxRegimes: ITaxRegimePublicServices; +} diff --git a/modules/catalogs/src/api/application/services/index.ts b/modules/catalogs/src/api/application/services/index.ts new file mode 100644 index 00000000..81746ced --- /dev/null +++ b/modules/catalogs/src/api/application/services/index.ts @@ -0,0 +1 @@ +export * from './catalog-public-services.interface'; diff --git a/modules/catalogs/src/api/application/tax-definitions/index.ts b/modules/catalogs/src/api/application/tax-definitions/index.ts new file mode 100644 index 00000000..f9c2a070 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/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-definitions/mappers/create-tax-definition-input.mapper.ts b/modules/catalogs/src/api/application/tax-definitions/mappers/create-tax-definition-input.mapper.ts new file mode 100644 index 00000000..e4920a10 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/mappers/create-tax-definition-input.mapper.ts @@ -0,0 +1,75 @@ +import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common"; +import { + DomainError, + TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionCreateProps, TaxDefinitionCode } from "../../../domain"; + +export interface ICreateTaxDefinitionInputMapper { + map( + dto: CreateTaxDefinitionRequestDTO, + params: { companyId: UniqueID } + ): Result<{ id: UniqueID; props: ITaxDefinitionCreateProps }, Error>; +} + +export class CreateTaxDefinitionInputMapper implements ICreateTaxDefinitionInputMapper { + public map( + dto: CreateTaxDefinitionRequestDTO, + params: { companyId: UniqueID } + ): Result<{ id: UniqueID; props: ITaxDefinitionCreateProps }, Error> { + const errors: ValidationErrorDetail[] = []; + + try { + const taxDefinitionId = extractOrPushError(UniqueID.create(dto.id), "id", errors); + + const code = extractOrPushError(TaxDefinitionCode.create(dto.code), "code", errors); + + const name = extractOrPushError(TextValue.create(dto.name), "name", 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: ITaxDefinitionCreateProps = { + companyId: params.companyId, + code: code!, + name: name!, + description: description!, + rate: dto.rate as any, + taxFamily: dto.tax_family as any, + calculationBehavior: dto.calculation_behavior as any, + jurisdictionCountryCode: dto.jurisdiction_country_code as any, + jurisdictionRegionCode: dto.jurisdiction_region_code as any, + taxScope: dto.tax_scope as any, + invoiceNote: dto.invoice_note as any, + allowedSurchargeCodes: dto.allowed_surcharge_codes as any, + isSystem: dto.is_system ?? false, + isActive: isActive ?? true, + validFrom: dto.valid_from as any, + validTo: dto.valid_to as any, + }; + + return Result.ok({ id: taxDefinitionId!, props }); + } catch (err: unknown) { + return Result.fail(new DomainError("Tax definition props mapping failed", { cause: err })); + } + } + + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Tax definition props mapping failed", errors); + } + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/mappers/index.ts b/modules/catalogs/src/api/application/tax-definitions/mappers/index.ts new file mode 100644 index 00000000..d4a1b8d5 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./create-tax-definition-input.mapper"; +export * from "./update-tax-definition-by-id-input.mapper"; diff --git a/modules/catalogs/src/api/application/tax-definitions/mappers/update-tax-definition-by-id-input.mapper.ts b/modules/catalogs/src/api/application/tax-definitions/mappers/update-tax-definition-by-id-input.mapper.ts new file mode 100644 index 00000000..13c1292a --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/mappers/update-tax-definition-by-id-input.mapper.ts @@ -0,0 +1,55 @@ +import type { UpdateTaxDefinitionByIdRequestDTO } from "@erp/catalogs/common"; +import { + DomainError, + TextValue, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { TaxDefinitionPatchProps } from "../../../domain"; + +export interface IUpdateTaxDefinitionByIdInputMapper { + map(dto: UpdateTaxDefinitionByIdRequestDTO): Result; +} + +export class UpdateTaxDefinitionByIdInputMapper implements IUpdateTaxDefinitionByIdInputMapper { + public map(dto: UpdateTaxDefinitionByIdRequestDTO): Result { + const errors: ValidationErrorDetail[] = []; + + try { + const name = extractOrPushError(TextValue.create(dto.name), "name", errors); + const description = extractOrPushError( + TextValue.create(dto.description), + "description", + errors + ); + + this.throwIfValidationErrors(errors); + + const props: TaxDefinitionPatchProps = { + name: name ?? undefined, + description: description ?? undefined, + invoiceNote: dto.invoice_note as any, + rate: dto.rate as any, + allowedSurchargeCodes: dto.allowed_surcharge_codes as any, + isActive: dto.is_active, + validFrom: dto.valid_from as any, + validTo: dto.valid_to as any, + }; + + return Result.ok(props); + } catch (err: unknown) { + return Result.fail( + new DomainError("Tax definition update props mapping failed", { cause: err }) + ); + } + } + + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Tax definition update props mapping failed", errors); + } + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/models/index.ts b/modules/catalogs/src/api/application/tax-definitions/models/index.ts new file mode 100644 index 00000000..6bc5497f --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/models/index.ts @@ -0,0 +1,2 @@ +export * from "./tax-definition-detail.model"; +export * from "./tax-definition-summary.model"; diff --git a/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-detail.model.ts b/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-detail.model.ts new file mode 100644 index 00000000..992b8a7b --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-detail.model.ts @@ -0,0 +1,23 @@ +import type { TaxDefinitionCode, TaxRate } from "@erp/catalogs/api/domain"; +import type { TextValue, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import type { Maybe } from "@repo/rdx-utils"; + +export type TaxDefinitionDetail = { + id: UniqueID; + companyId: UniqueID; + code: TaxDefinitionCode; + name: TextValue; + description: Maybe; + rate: TaxRate; + taxFamily: string; + calculationBehavior: string; + jurisdictionCountryCode: string; + jurisdictionRegionCode: Maybe; + taxScope: string; + invoiceNote: Maybe; + allowedSurchargeCodes: Maybe; + isSystem: boolean; + isActive: boolean; + validFrom: Maybe; + validTo: Maybe; +}; diff --git a/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-summary.model.ts b/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-summary.model.ts new file mode 100644 index 00000000..51e9d319 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/models/tax-definition-summary.model.ts @@ -0,0 +1,11 @@ +import type { TaxDefinitionCode } from "@erp/catalogs/api/domain"; +import type { TextValue, UniqueID } from "@repo/rdx-ddd"; + +export type TaxDefinitionSummary = { + id: UniqueID; + companyId: UniqueID; + code: TaxDefinitionCode; + name: TextValue; + isActive: boolean; + isSystem: boolean; +}; diff --git a/modules/catalogs/src/api/application/tax-definitions/repositories/index.ts b/modules/catalogs/src/api/application/tax-definitions/repositories/index.ts new file mode 100644 index 00000000..aecaa2c7 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/repositories/index.ts @@ -0,0 +1 @@ +export * from './tax-definition-repository.interface'; diff --git a/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts b/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-repository.interface.ts new file mode 100644 index 00000000..389294ba --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/repositories/tax-definition-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 { TaxDefinition, TaxDefinitionCode } from "../../../domain"; +import type { TaxDefinitionSummary } from "../../tax-definitions/models"; + +export interface ITaxDefinitionRepository { + create(taxDefinition: TaxDefinition, transaction?: unknown): Promise>; + update(taxDefinition: TaxDefinition, 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: TaxDefinitionCode, + transaction?: unknown + ): Promise>; + + findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: unknown + ): Promise, Error>>; +} diff --git a/modules/catalogs/src/api/application/tax-definitions/services/index.ts b/modules/catalogs/src/api/application/tax-definitions/services/index.ts new file mode 100644 index 00000000..fa60b209 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/index.ts @@ -0,0 +1,5 @@ +export * from "./tax-definition-creator.service"; +export * from "./tax-definition-deleter.service"; +export * from "./tax-definition-finder.service"; +export * from "./tax-definition-status-changer.service"; +export * from "./tax-definition-updater.service"; diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-creator.service.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-creator.service.ts new file mode 100644 index 00000000..b596500f --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-creator.service.ts @@ -0,0 +1,37 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { type ITaxDefinitionCreateProps, TaxDefinition } from "../../../domain"; +import type { ITaxDefinitionRepository } from "../repositories"; + +export interface ITaxDefinitionCreatorParams { + companyId: UniqueID; + id: UniqueID; + props: ITaxDefinitionCreateProps; + transaction: unknown; +} + +export interface ITaxDefinitionCreator { + create(params: ITaxDefinitionCreatorParams): Promise>; +} + +export class TaxDefinitionCreator implements ITaxDefinitionCreator { + constructor(private readonly repository: ITaxDefinitionRepository) {} + + public async create(params: ITaxDefinitionCreatorParams): Promise> { + const { companyId, id, props, transaction } = params; + + const taxDefinitionResult = TaxDefinition.create({ ...props, companyId }, id); + if (taxDefinitionResult.isFailure) { + return taxDefinitionResult; + } + + const taxDefinition = taxDefinitionResult.data; + const saveResult = await this.repository.create(taxDefinition, transaction); + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(taxDefinition); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-deleter.service.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-deleter.service.ts new file mode 100644 index 00000000..4fd53385 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-deleter.service.ts @@ -0,0 +1,20 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionRepository } from "../repositories"; + +export interface ITaxDefinitionDeleter { + deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; +} + +export class TaxDefinitionDeleter implements ITaxDefinitionDeleter { + constructor(private readonly repository: ITaxDefinitionRepository) {} + + public async deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: unknown) { + return this.repository.deleteByIdInCompany(companyId, id, transaction); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-finder.service.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-finder.service.ts new file mode 100644 index 00000000..a56f361d --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-finder.service.ts @@ -0,0 +1,21 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { TaxDefinition } from "../../../domain"; +import type { ITaxDefinitionRepository } from "../repositories"; + +export interface ITaxDefinitionFinder { + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; +} + +export class TaxDefinitionFinder implements ITaxDefinitionFinder { + constructor(private readonly repository: ITaxDefinitionRepository) {} + + public async getByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: unknown) { + return this.repository.getByIdInCompany(companyId, id, transaction); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-status-changer.service.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-status-changer.service.ts new file mode 100644 index 00000000..4738121e --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-status-changer.service.ts @@ -0,0 +1,49 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionRepository } from "../repositories"; + +export interface ITaxDefinitionStatusChanger { + enable(companyId: UniqueID, id: UniqueID, transaction?: unknown): Promise>; + disable( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise>; +} + +export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger { + constructor(private readonly repository: ITaxDefinitionRepository) {} + + public async enable(companyId: UniqueID, id: UniqueID, transaction?: unknown) { + const existing = await this.repository.getByIdInCompany(companyId, id, transaction); + if (existing.isFailure) return Result.fail(existing.error); + + const taxDefinition = existing.data; + const changeResult = taxDefinition.enable(); + if (changeResult.isFailure) return Result.fail(changeResult.error); + + if (changeResult.data) { + const saveResult = await this.repository.update(taxDefinition, transaction); + if (saveResult.isFailure) return Result.fail(saveResult.error); + } + + return Result.ok(changeResult.data); + } + + public async disable(companyId: UniqueID, id: UniqueID, transaction?: unknown) { + const existing = await this.repository.getByIdInCompany(companyId, id, transaction); + if (existing.isFailure) return Result.fail(existing.error); + + const taxDefinition = existing.data; + const changeResult = taxDefinition.disable(); + if (changeResult.isFailure) return Result.fail(changeResult.error); + + if (changeResult.data) { + const saveResult = await this.repository.update(taxDefinition, transaction); + if (saveResult.isFailure) return Result.fail(saveResult.error); + } + + return Result.ok(changeResult.data); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-updater.service.ts b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-updater.service.ts new file mode 100644 index 00000000..27250b0b --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/services/tax-definition-updater.service.ts @@ -0,0 +1,39 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { TaxDefinition, TaxDefinitionPatchProps } from "../../../domain"; +import type { ITaxDefinitionRepository } from "../repositories"; + +export interface ITaxDefinitionUpdaterParams { + companyId: UniqueID; + id: UniqueID; + patch: TaxDefinitionPatchProps; + transaction: unknown; +} + +export interface ITaxDefinitionUpdater { + update(params: ITaxDefinitionUpdaterParams): Promise>; +} + +export class TaxDefinitionUpdater implements ITaxDefinitionUpdater { + constructor(private readonly repository: ITaxDefinitionRepository) {} + + public async update(params: ITaxDefinitionUpdaterParams): Promise> { + const { companyId, id, patch, transaction } = params; + + const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction); + if (existingResult.isFailure) return Result.fail(existingResult.error); + + const taxDefinition = existingResult.data; + + const updateResult = taxDefinition.update(patch); + if (updateResult.isFailure) return Result.fail(updateResult.error); + + if (updateResult.data) { + const saveResult = await this.repository.update(taxDefinition, transaction); + if (saveResult.isFailure) return Result.fail(saveResult.error); + } + + return Result.ok(taxDefinition); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts new file mode 100644 index 00000000..1d058f4a --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.ts @@ -0,0 +1,49 @@ +import type { TaxDefinitionDetailDTO } from "@erp/catalogs/common"; +import type { ISnapshotBuilder } from "@erp/core/api"; + +import type { TaxDefinitionDetail } from "../../models"; + +export interface ITaxDefinitionFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class TaxDefinitionFullSnapshotBuilder implements ITaxDefinitionFullSnapshotBuilder { + public toOutput(src: TaxDefinitionDetail): TaxDefinitionDetailDTO { + return { + id: src.id.toString(), + company_id: src.companyId.toString(), + code: src.code.toPrimitive(), + name: src.name.toPrimitive(), + description: src.description.match( + (v) => v.toPrimitive(), + () => null + ), + rate: (src.rate as any).toPrimitive(), + tax_family: src.taxFamily, + calculation_behavior: src.calculationBehavior, + jurisdiction_country_code: src.jurisdictionCountryCode, + jurisdiction_region_code: src.jurisdictionRegionCode.match( + (v) => v, + () => null + ), + tax_scope: src.taxScope, + invoice_note: src.invoiceNote.match( + (v) => v.toPrimitive(), + () => null + ), + allowed_surcharge_codes: src.allowedSurchargeCodes.match( + (arr) => arr, + () => null + ), + is_system: src.isSystem, + is_active: src.isActive, + valid_from: src.validFrom.match( + (d) => d.toPrimitive(), + () => null + ), + valid_to: src.validTo.match( + (d) => d.toPrimitive(), + () => null + ), + } as unknown as TaxDefinitionDetailDTO; + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/index.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/index.ts new file mode 100644 index 00000000..1d002deb --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/index.ts @@ -0,0 +1 @@ +export * from "./full-tax-definition-snapshot.builder"; diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/index.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/index.ts new file mode 100644 index 00000000..3b83e1ff --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/index.ts @@ -0,0 +1,2 @@ +export * from "./full"; +export * from "./summary"; diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/index.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/index.ts new file mode 100644 index 00000000..72ebb252 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/index.ts @@ -0,0 +1 @@ +export * from "./summary-tax-definition-snapshot.builder"; diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/summary-tax-definition-snapshot.builder.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/summary-tax-definition-snapshot.builder.ts new file mode 100644 index 00000000..9d25551a --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/summary/summary-tax-definition-snapshot.builder.ts @@ -0,0 +1,20 @@ +import type { TaxDefinitionSummaryDTO } from "@erp/catalogs/common"; +import type { ISnapshotBuilder } from "@erp/core/api"; + +import type { TaxDefinitionSummary } from "../../models"; + +export interface ITaxDefinitionSummarySnapshotBuilder + extends ISnapshotBuilder {} + +export class TaxDefinitionSummarySnapshotBuilder implements ITaxDefinitionSummarySnapshotBuilder { + public toOutput(src: TaxDefinitionSummary): TaxDefinitionSummaryDTO { + return { + id: src.id.toString(), + company_id: src.companyId.toString(), + code: src.code.toPrimitive(), + name: src.name.toPrimitive(), + is_active: src.isActive, + is_system: src.isSystem, + }; + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/create-tax-definition.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/create-tax-definition.use-case.ts new file mode 100644 index 00000000..1fa41ee9 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/create-tax-definition.use-case.ts @@ -0,0 +1,55 @@ +import type { CreateTaxDefinitionRequestDTO } 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 { ICreateTaxDefinitionInputMapper } from "../mappers"; +import type { ITaxDefinitionCreator } from "../services"; +import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders"; + +export type CreateTaxDefinitionUseCaseInput = { + companyId: UniqueID; + dto: CreateTaxDefinitionRequestDTO; +}; + +type CreateTaxDefinitionUseCaseDeps = { + dtoMapper: ICreateTaxDefinitionInputMapper; + creator: ITaxDefinitionCreator; + fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder; + transactionManager: ITransactionManager; +}; + +export class CreateTaxDefinitionUseCase { + constructor(private readonly deps: CreateTaxDefinitionUseCaseDeps) {} + + public execute(params: CreateTaxDefinitionUseCaseInput) { + 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 as any); + return Result.ok(snapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.ts new file mode 100644 index 00000000..8a8bab71 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.ts @@ -0,0 +1,19 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionDeleter } from "../services"; + +type DeleteTaxDefinitionDeps = { + deleter: ITaxDefinitionDeleter; +}; + +export class DeleteTaxDefinitionByIdUseCase { + constructor(private readonly deps: DeleteTaxDefinitionDeps) {} + + public async execute(companyId: UniqueID, id: UniqueID) { + const result = await this.deps.deleter.deleteByIdInCompany(companyId, id); + if (result.isFailure) return result; + + return Result.ok(result.data); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/disable-tax-definition-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/disable-tax-definition-by-id.use-case.ts new file mode 100644 index 00000000..6ea61810 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/disable-tax-definition-by-id.use-case.ts @@ -0,0 +1,19 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionStatusChanger } from "../services"; + +type DisableTaxDefinitionDeps = { + statusChanger: ITaxDefinitionStatusChanger; +}; + +export class DisableTaxDefinitionByIdUseCase { + constructor(private readonly deps: DisableTaxDefinitionDeps) {} + + public async execute(companyId: UniqueID, id: UniqueID) { + const result = await this.deps.statusChanger.disable(companyId, id); + if (result.isFailure) return result; + + return Result.ok(result.data); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/enable-tax-definition-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/enable-tax-definition-by-id.use-case.ts new file mode 100644 index 00000000..2a48d635 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/enable-tax-definition-by-id.use-case.ts @@ -0,0 +1,19 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionStatusChanger } from "../services"; + +type EnableTaxDefinitionDeps = { + statusChanger: ITaxDefinitionStatusChanger; +}; + +export class EnableTaxDefinitionByIdUseCase { + constructor(private readonly deps: EnableTaxDefinitionDeps) {} + + public async execute(companyId: UniqueID, id: UniqueID) { + const result = await this.deps.statusChanger.enable(companyId, id); + if (result.isFailure) return result; + + return Result.ok(result.data); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/get-tax-definition-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/get-tax-definition-by-id.use-case.ts new file mode 100644 index 00000000..8bae2eb8 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/get-tax-definition-by-id.use-case.ts @@ -0,0 +1,22 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionFinder } from "../services"; +import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders"; + +type GetTaxDefinitionByIdUseCaseDeps = { + finder: ITaxDefinitionFinder; + fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder; +}; + +export class GetTaxDefinitionByIdUseCase { + constructor(private readonly deps: GetTaxDefinitionByIdUseCaseDeps) {} + + public async execute(companyId: UniqueID, id: UniqueID) { + const found = await this.deps.finder.getByIdInCompany(companyId, id); + if (found.isFailure) return found; + + const snapshot = this.deps.fullSnapshotBuilder.toOutput(found.data as any); + return Result.ok(snapshot); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/index.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/index.ts new file mode 100644 index 00000000..d3dd0471 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/index.ts @@ -0,0 +1,7 @@ +export * from "./create-tax-definition.use-case"; +export * from "./delete-tax-definition-by-id.use-case"; +export * from "./disable-tax-definition-by-id.use-case"; +export * from "./enable-tax-definition-by-id.use-case"; +export * from "./get-tax-definition-by-id.use-case"; +export * from "./list-tax-definitions.use-case"; +export * from "./update-tax-definition-by-id.use-case"; diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/list-tax-definitions.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/list-tax-definitions.use-case.ts new file mode 100644 index 00000000..3977074c --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/list-tax-definitions.use-case.ts @@ -0,0 +1,23 @@ +import type { Criteria } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ITaxDefinitionRepository } from "../repositories"; +import type { ITaxDefinitionSummarySnapshotBuilder } from "../snapshot-builders"; + +type ListTaxDefinitionsUseCaseDeps = { + repository: ITaxDefinitionRepository; + summarySnapshotBuilder: ITaxDefinitionSummarySnapshotBuilder; +}; + +export class ListTaxDefinitionsUseCase { + constructor(private readonly deps: ListTaxDefinitionsUseCaseDeps) {} + + public async execute(companyId: UniqueID, criteria: Criteria) { + const found = await this.deps.repository.findByCriteriaInCompany(companyId, criteria); + if (found.isFailure) return found; + + const collection = found.data.map((item) => this.deps.summarySnapshotBuilder.toOutput(item)); + return Result.ok(collection); + } +} diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/update-tax-definition-by-id.use-case.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/update-tax-definition-by-id.use-case.ts new file mode 100644 index 00000000..16ec4203 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/update-tax-definition-by-id.use-case.ts @@ -0,0 +1,37 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IUpdateTaxDefinitionByIdInputMapper } from "../mappers"; +import type { ITaxDefinitionUpdater } from "../services"; +import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders"; + +type UpdateTaxDefinitionByIdDeps = { + dtoMapper: IUpdateTaxDefinitionByIdInputMapper; + updater: ITaxDefinitionUpdater; + fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder; +}; + +export type UpdateTaxDefinitionByIdUseCaseInput = { + id: UniqueID; + dto: unknown; +}; + +export class UpdateTaxDefinitionByIdUseCase { + constructor(private readonly deps: UpdateTaxDefinitionByIdDeps) {} + + public async execute(companyId: UniqueID, params: UpdateTaxDefinitionByIdUseCaseInput) { + const mapped = this.deps.dtoMapper.map(params.dto as any); + if (mapped.isFailure) return mapped; + + const result = await this.deps.updater.update({ + companyId, + id: params.id, + patch: mapped.data, + transaction: undefined, + }); + if (result.isFailure) return result; + + const snapshot = this.deps.fullSnapshotBuilder.toOutput(result.data as any); + return Result.ok(snapshot); + } +} diff --git a/modules/catalogs/src/api/domain/index.ts b/modules/catalogs/src/api/domain/index.ts index 23f8279d..1702efcf 100644 --- a/modules/catalogs/src/api/domain/index.ts +++ b/modules/catalogs/src/api/domain/index.ts @@ -1,3 +1,4 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-definitions"; export * from "./tax-regimes"; diff --git a/modules/catalogs/src/api/domain/tax-definitions/calculation-behavior.ts b/modules/catalogs/src/api/domain/tax-definitions/calculation-behavior.ts new file mode 100644 index 00000000..15770756 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/calculation-behavior.ts @@ -0,0 +1,34 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionCalculationBehaviorError } from "./errors"; + +export type TaxCalculationBehaviorType = "additive" | "subtractive" | "neutral"; + +export class TaxCalculationBehavior { + private constructor(private readonly value: TaxCalculationBehaviorType) {} + + public static create(value: string): Result { + const v = value?.toString() ?? ""; + const allowed: TaxCalculationBehaviorType[] = ["additive", "subtractive", "neutral"]; + + if (!allowed.includes(v as TaxCalculationBehaviorType)) { + return Result.fail( + new InvalidTaxDefinitionCalculationBehaviorError("Invalid calculation behavior") + ); + } + + return Result.ok(new TaxCalculationBehavior(v as TaxCalculationBehaviorType)); + } + + public static fromPersistence(value: string): TaxCalculationBehavior { + return new TaxCalculationBehavior(value as TaxCalculationBehaviorType); + } + + public toPrimitive(): TaxCalculationBehaviorType { + return this.value; + } + + public toString(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/errors.ts b/modules/catalogs/src/api/domain/tax-definitions/errors.ts new file mode 100644 index 00000000..903b427c --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/errors.ts @@ -0,0 +1,129 @@ +import { DomainError } from "@repo/rdx-ddd"; + +export class InvalidTaxDefinitionIdError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_ID" as const; +} + +export const isInvalidTaxDefinitionIdError = (e: unknown): e is InvalidTaxDefinitionIdError => + e instanceof InvalidTaxDefinitionIdError; + +export class InvalidTaxDefinitionCompanyIdError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_COMPANY_ID" as const; +} + +export const isInvalidTaxDefinitionCompanyIdError = ( + e: unknown +): e is InvalidTaxDefinitionCompanyIdError => e instanceof InvalidTaxDefinitionCompanyIdError; + +export class InvalidTaxDefinitionCodeError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_CODE" as const; +} + +export const isInvalidTaxDefinitionCodeError = (e: unknown): e is InvalidTaxDefinitionCodeError => + e instanceof InvalidTaxDefinitionCodeError; + +export class InvalidTaxDefinitionNameError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_NAME" as const; +} + +export const isInvalidTaxDefinitionNameError = (e: unknown): e is InvalidTaxDefinitionNameError => + e instanceof InvalidTaxDefinitionNameError; + +export class InvalidTaxDefinitionDescriptionError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_DESCRIPTION" as const; +} + +export const isInvalidTaxDefinitionDescriptionError = ( + e: unknown +): e is InvalidTaxDefinitionDescriptionError => e instanceof InvalidTaxDefinitionDescriptionError; + +export class InvalidTaxDefinitionRateError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_RATE" as const; +} + +export const isInvalidTaxDefinitionRateError = (e: unknown): e is InvalidTaxDefinitionRateError => + e instanceof InvalidTaxDefinitionRateError; + +export class InvalidTaxDefinitionFamilyError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_FAMILY" as const; +} + +export const isInvalidTaxDefinitionFamilyError = ( + e: unknown +): e is InvalidTaxDefinitionFamilyError => e instanceof InvalidTaxDefinitionFamilyError; + +export class InvalidTaxDefinitionCalculationBehaviorError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_CALCULATION_BEHAVIOR" as const; +} + +export const isInvalidTaxDefinitionCalculationBehaviorError = ( + e: unknown +): e is InvalidTaxDefinitionCalculationBehaviorError => + e instanceof InvalidTaxDefinitionCalculationBehaviorError; + +export class InvalidTaxDefinitionJurisdictionCountryCodeError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_COUNTRY_CODE" as const; +} + +export const isInvalidTaxDefinitionJurisdictionCountryCodeError = ( + e: unknown +): e is InvalidTaxDefinitionJurisdictionCountryCodeError => + e instanceof InvalidTaxDefinitionJurisdictionCountryCodeError; + +export class InvalidTaxDefinitionJurisdictionRegionCodeError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_REGION_CODE" as const; +} + +export const isInvalidTaxDefinitionJurisdictionRegionCodeError = ( + e: unknown +): e is InvalidTaxDefinitionJurisdictionRegionCodeError => + e instanceof InvalidTaxDefinitionJurisdictionRegionCodeError; + +export class InvalidTaxDefinitionScopeError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_SCOPE" as const; +} + +export const isInvalidTaxDefinitionScopeError = (e: unknown): e is InvalidTaxDefinitionScopeError => + e instanceof InvalidTaxDefinitionScopeError; + +export class InvalidTaxDefinitionInvoiceNoteError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_INVOICE_NOTE" as const; +} + +export const isInvalidTaxDefinitionInvoiceNoteError = ( + e: unknown +): e is InvalidTaxDefinitionInvoiceNoteError => e instanceof InvalidTaxDefinitionInvoiceNoteError; + +export class InvalidTaxDefinitionAllowedSurchargeCodesError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_ALLOWED_SURCHARGE_CODES" as const; +} + +export const isInvalidTaxDefinitionAllowedSurchargeCodesError = ( + e: unknown +): e is InvalidTaxDefinitionAllowedSurchargeCodesError => + e instanceof InvalidTaxDefinitionAllowedSurchargeCodesError; + +export class InvalidTaxDefinitionValidityPeriodError extends DomainError { + public readonly code = "TAX_DEFINITION_INVALID_VALIDITY_PERIOD" as const; +} + +export const isInvalidTaxDefinitionValidityPeriodError = ( + e: unknown +): e is InvalidTaxDefinitionValidityPeriodError => + e instanceof InvalidTaxDefinitionValidityPeriodError; + +export class TaxDefinitionCannotBeEnabledError extends DomainError { + public readonly code = "TAX_DEFINITION_CANNOT_BE_ENABLED" as const; +} + +export const isTaxDefinitionCannotBeEnabledError = ( + e: unknown +): e is TaxDefinitionCannotBeEnabledError => e instanceof TaxDefinitionCannotBeEnabledError; + +export class TaxDefinitionCannotBeDisabledError extends DomainError { + public readonly code = "TAX_DEFINITION_CANNOT_BE_DISABLED" as const; +} + +export const isTaxDefinitionCannotBeDisabledError = ( + e: unknown +): e is TaxDefinitionCannotBeDisabledError => e instanceof TaxDefinitionCannotBeDisabledError; diff --git a/modules/catalogs/src/api/domain/tax-definitions/index.ts b/modules/catalogs/src/api/domain/tax-definitions/index.ts new file mode 100644 index 00000000..47ec912b --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/index.ts @@ -0,0 +1,10 @@ +export * from "./calculation-behavior"; +export * from "./errors"; +export * from "./jurisdiction-country-code"; +export * from "./jurisdiction-region-code"; +export * from "./tax-definition.aggregate"; +export * from "./tax-definition-code"; +export * from "./tax-definition-name"; +export * from "./tax-family"; +export * from "./tax-rate"; +export * from "./tax-scope"; diff --git a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts new file mode 100644 index 00000000..a3fbda89 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-country-code.ts @@ -0,0 +1,41 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionJurisdictionCountryCodeError } from "./errors"; + +export class TaxJurisdictionCountryCode { + private constructor(private readonly value: string) {} + + public static create(code: string): Result { + const trimmed = code?.trim() ?? ""; + if (trimmed.length !== 2) { + return Result.fail( + new InvalidTaxDefinitionJurisdictionCountryCodeError("Country code must be 2 letters") + ); + } + + const normalized = trimmed.toUpperCase(); + const regex = /^[A-Z]{2}$/; + if (!regex.test(normalized)) { + return Result.fail( + new InvalidTaxDefinitionJurisdictionCountryCodeError( + "Country code must be 2 uppercase letters" + ) + ); + } + + // NOTE: We allow two-letter codes including 'EU' to support union-level definitions like reverse charge. + return Result.ok(new TaxJurisdictionCountryCode(normalized)); + } + + public static fromPersistence(code: string): TaxJurisdictionCountryCode { + return new TaxJurisdictionCountryCode(code); + } + + public toPrimitive(): string { + return this.value; + } + + public toString(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts new file mode 100644 index 00000000..58a60136 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/jurisdiction-region-code.ts @@ -0,0 +1,42 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionJurisdictionRegionCodeError } from "./errors"; + +export class TaxJurisdictionRegionCode { + private constructor(private readonly value: string) {} + + public static create(code: string): Result { + const trimmed = code?.trim() ?? ""; + if (trimmed.length === 0) { + return Result.fail( + new InvalidTaxDefinitionJurisdictionRegionCodeError("Region code cannot be empty") + ); + } + + const normalized = trimmed.toUpperCase(); + + // basic pattern: CC-... (we won't validate full ISO-3166-2 list) + const regex = /^[A-Z]{2}-[A-Z0-9-]+$/; + if (!regex.test(normalized)) { + return Result.fail( + new InvalidTaxDefinitionJurisdictionRegionCodeError( + "Region code must follow ISO-3166-2 pattern COUNTRY-REGION" + ) + ); + } + + return Result.ok(new TaxJurisdictionRegionCode(normalized)); + } + + public static fromPersistence(code: string): TaxJurisdictionRegionCode { + return new TaxJurisdictionRegionCode(code); + } + + public toPrimitive(): string { + return this.value; + } + + public toString(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-definition-code.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-definition-code.ts new file mode 100644 index 00000000..2ffc1523 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition-code.ts @@ -0,0 +1,36 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionCodeError } from "./errors"; + +export class TaxDefinitionCode { + private constructor(private readonly value: string) {} + + public static create(code: string): Result { + const trimmed = code?.trim() ?? ""; + if (trimmed.length === 0) { + return Result.fail(new InvalidTaxDefinitionCodeError("Tax definition code cannot be empty")); + } + + const normalized = trimmed.toLowerCase(); + const regex = /^[a-z0-9][a-z0-9_]*$/; + if (!regex.test(normalized)) { + return Result.fail( + new InvalidTaxDefinitionCodeError("Tax definition code must match ^[a-z0-9][a-z0-9_]*$") + ); + } + + return Result.ok(new TaxDefinitionCode(normalized)); + } + + public static fromPersistence(code: string): TaxDefinitionCode { + return new TaxDefinitionCode(code); + } + + public toString(): string { + return this.value; + } + + public toPrimitive(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-definition-name.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-definition-name.ts new file mode 100644 index 00000000..59849001 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition-name.ts @@ -0,0 +1,29 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionNameError } from "./errors"; + +export class TaxDefinitionName { + private constructor(private readonly value: string) {} + + public static create(name: string): Result { + const trimmed = name?.trim() ?? ""; + if (trimmed.length === 0) { + return Result.fail(new InvalidTaxDefinitionNameError("Tax definition name cannot be empty")); + } + + // follow existing patterns: no extra length validation here + return Result.ok(new TaxDefinitionName(trimmed)); + } + + public static fromPersistence(name: string): TaxDefinitionName { + return new TaxDefinitionName(name); + } + + public toString(): string { + return this.value; + } + + public toPrimitive(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts new file mode 100644 index 00000000..ab97167c --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts @@ -0,0 +1,369 @@ +import { AggregateRoot, type TextValue, type UniqueID, type UtcDate } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { TaxCalculationBehavior } from "./calculation-behavior"; +import { + InvalidTaxDefinitionAllowedSurchargeCodesError, + InvalidTaxDefinitionJurisdictionRegionCodeError, + InvalidTaxDefinitionValidityPeriodError, +} from "./errors"; +import type { TaxJurisdictionCountryCode } from "./jurisdiction-country-code"; +import type { TaxJurisdictionRegionCode } from "./jurisdiction-region-code"; +import type { TaxDefinitionCode } from "./tax-definition-code"; +import type { TaxDefinitionName } from "./tax-definition-name"; +import type { TaxFamily } from "./tax-family"; +import type { TaxRate } from "./tax-rate"; +import type { TaxScope } from "./tax-scope"; + +export interface ITaxDefinitionCreateProps { + companyId: UniqueID; + code: TaxDefinitionCode; + name: TaxDefinitionName; + description: Maybe; + rate: TaxRate; + taxFamily: TaxFamily; + calculationBehavior: TaxCalculationBehavior; + jurisdictionCountryCode: TaxJurisdictionCountryCode; + jurisdictionRegionCode: Maybe; + taxScope: TaxScope; + invoiceNote: Maybe; // Texto fiscal que aparece en el documento + allowedSurchargeCodes: Maybe; + isSystem: boolean; + isActive: boolean; + validFrom: Maybe; + validTo: Maybe; +} + +export type TaxDefinitionPatchProps = Partial<{ + name: TaxDefinitionName; + description: Maybe; + rate: TaxRate; + invoiceNote: Maybe; + allowedSurchargeCodes: Maybe; + isActive: boolean; + validFrom: Maybe; + validTo: Maybe; +}>; + +export type TaxDefinitionInternalProps = ITaxDefinitionCreateProps; + +export class TaxDefinition extends AggregateRoot { + protected constructor(props: TaxDefinitionInternalProps, id?: UniqueID) { + super(props, id); + } + + public static create( + props: ITaxDefinitionCreateProps, + id?: UniqueID + ): Result { + const validationResult = TaxDefinition.validateCreateProps(props); + + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + + const taxDefinition = new TaxDefinition(props, id); + + return Result.ok(taxDefinition); + } + + public static rehydrate(props: TaxDefinitionInternalProps, id: UniqueID): TaxDefinition { + return new TaxDefinition(props, id); + } + + private static validateCreateProps(props: ITaxDefinitionCreateProps): Result { + if (!props.companyId) { + return Result.fail(new Error("Tax definition company ID is required")); + } + + if (!props.code) { + return Result.fail(new Error("Tax definition code is required")); + } + + if (!props.name) { + return Result.fail(new Error("Tax definition name is required")); + } + + if (!props.rate) { + return Result.fail(new Error("Tax definition rate is required")); + } + + if (!props.taxFamily) { + return Result.fail(new Error("Tax definition family is required")); + } + + if (!props.calculationBehavior) { + return Result.fail(new Error("Tax definition calculation behavior is required")); + } + + if (!props.jurisdictionCountryCode) { + return Result.fail(new Error("Tax definition jurisdiction country code is required")); + } + + if (!props.taxScope) { + return Result.fail(new Error("Tax definition scope is required")); + } + + // allowedSurchargeCodes rules + if (props.allowedSurchargeCodes && props.allowedSurchargeCodes.isSome()) { + const arr = props.allowedSurchargeCodes.unwrap(); + // duplicates + const primitives = arr.map((c) => c.toPrimitive()); + const unique = Array.from(new Set(primitives)); + if (unique.length !== primitives.length) { + return Result.fail( + new InvalidTaxDefinitionAllowedSurchargeCodesError("Duplicate surcharge codes") + ); + } + + // cannot reference itself + if (primitives.includes(props.code.toPrimitive())) { + return Result.fail( + new InvalidTaxDefinitionAllowedSurchargeCodesError( + "Surcharge codes cannot contain the tax's own code" + ) + ); + } + + // only allowed for 'iva' + if (props.taxFamily.toPrimitive() !== "iva") { + return Result.fail( + new InvalidTaxDefinitionAllowedSurchargeCodesError( + "Allowed surcharge codes only valid for 'iva' family" + ) + ); + } + } + + // validFrom/validTo coherence + if (props.validFrom && props.validFrom.isSome() && props.validTo && props.validTo.isSome()) { + const from = props.validFrom.unwrap(); + const to = props.validTo.unwrap(); + if (from.toPrimitive() > to.toPrimitive()) { + return Result.fail( + new InvalidTaxDefinitionValidityPeriodError("validFrom must be <= validTo") + ); + } + } + + // region coherence with country + if (props.jurisdictionRegionCode && props.jurisdictionRegionCode.isSome()) { + const region = props.jurisdictionRegionCode.unwrap().toPrimitive(); + const country = props.jurisdictionCountryCode.toPrimitive(); + if (!region.startsWith(`${country}-`)) { + return Result.fail( + new InvalidTaxDefinitionJurisdictionRegionCodeError( + "Region code must start with country code" + ) + ); + } + } + + return Result.ok(); + } + + private static validatePatchProps(patchProps: TaxDefinitionPatchProps): Result { + if (Object.keys(patchProps).length === 0) { + return Result.ok(); + } + + return Result.ok(); + } + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get code(): TaxDefinitionCode { + return this.props.code; + } + + public get name(): TaxDefinitionName { + return this.props.name; + } + + public get description(): Maybe { + return this.props.description; + } + + public get rate(): TaxRate { + return this.props.rate as TaxRate; + } + + public get taxFamily(): TaxFamily { + return this.props.taxFamily; + } + + public get calculationBehavior(): TaxCalculationBehavior { + return this.props.calculationBehavior; + } + + public get jurisdictionCountryCode(): TaxJurisdictionCountryCode { + return this.props.jurisdictionCountryCode; + } + + public get jurisdictionRegionCode(): Maybe { + return this.props.jurisdictionRegionCode; + } + + public get taxScope(): TaxScope { + return this.props.taxScope; + } + + public get invoiceNote(): Maybe { + return this.props.invoiceNote; + } + + public get allowedSurchargeCodes(): Maybe { + return this.props.allowedSurchargeCodes; + } + + public get isSystem(): boolean { + return this.props.isSystem; + } + + public get isActive(): boolean { + return this.props.isActive; + } + + public get validFrom(): Maybe { + return this.props.validFrom; + } + + public get validTo(): Maybe { + return this.props.validTo; + } + + public update(patchProps: TaxDefinitionPatchProps): Result { + const validationResult = TaxDefinition.validatePatchProps(patchProps); + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + + let hasChanges = false; + + // Only allow updating mutable fields + if ( + patchProps.name !== undefined && + this.props.name.toPrimitive() !== patchProps.name.toPrimitive() + ) { + this.props.name = patchProps.name; + hasChanges = true; + } + + if ( + patchProps.description !== undefined && + !TaxDefinition.sameDescription(this.props.description, patchProps.description) + ) { + this.props.description = patchProps.description; + hasChanges = true; + } + + if ( + patchProps.rate !== undefined && + this.props.rate.toPrimitive() !== patchProps.rate.toPrimitive() + ) { + this.props.rate = patchProps.rate as unknown as any; + hasChanges = true; + } + + if ( + patchProps.invoiceNote !== undefined && + !TaxDefinition.sameDescription(this.props.invoiceNote, patchProps.invoiceNote) + ) { + this.props.invoiceNote = patchProps.invoiceNote; + hasChanges = true; + } + + if (patchProps.allowedSurchargeCodes !== undefined) { + this.props.allowedSurchargeCodes = patchProps.allowedSurchargeCodes; + hasChanges = true; + } + + if (patchProps.isActive !== undefined && this.props.isActive !== patchProps.isActive) { + this.props.isActive = patchProps.isActive; + hasChanges = true; + } + + if (patchProps.validFrom !== undefined) { + this.props.validFrom = patchProps.validFrom; + hasChanges = true; + } + + if (patchProps.validTo !== undefined) { + this.props.validTo = patchProps.validTo; + hasChanges = true; + } + + return Result.ok(hasChanges); + } + + public disable(): Result { + if (!this.isActive) { + return Result.ok(false); + } + + this.props.isActive = false; + return Result.ok(true); + } + + public enable(): Result { + if (this.isActive) { + return Result.ok(false); + } + + this.props.isActive = true; + return Result.ok(true); + } + + public toJSON() { + return { + id: this.id.toPrimitive(), + company_id: this.companyId.toPrimitive(), + code: this.code.toPrimitive(), + name: this.name.toPrimitive(), + description: this.description.match( + (v) => v.toPrimitive(), + () => null + ), + rate: { value: this.rate.toPrimitive(), scale: (this.rate as any).scale ?? 2 }, + tax_family: this.taxFamily.toPrimitive(), + calculation_behavior: this.calculationBehavior.toPrimitive(), + jurisdiction_country_code: this.jurisdictionCountryCode.toPrimitive(), + jurisdiction_region_code: this.jurisdictionRegionCode.match( + (v) => v.toPrimitive(), + () => null + ), + tax_scope: this.taxScope.toPrimitive(), + invoice_note: this.invoiceNote.match( + (v) => v.toPrimitive(), + () => null + ), + allowed_surcharge_codes: this.allowedSurchargeCodes.match( + (arr) => arr.map((c) => c.toPrimitive()), + () => null + ), + is_system: this.isSystem, + is_active: this.isActive, + valid_from: this.validFrom.match( + (d) => d.toPrimitive(), + () => null + ), + valid_to: this.validTo.match( + (d) => d.toPrimitive(), + () => null + ), + }; + } + + private static sameDescription(current: Maybe, next: Maybe): boolean { + return current.match( + (currentValue) => + next.match( + (nextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(), + () => false + ), + () => next.isNone() + ); + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-family.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-family.ts new file mode 100644 index 00000000..3c98bc94 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-family.ts @@ -0,0 +1,57 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionFamilyError } from "./errors"; + +export type TaxFamilyType = + | "iva" + | "igic" + | "ipsi" + | "equivalence_surcharge" + | "withholding" + | "vat" + | "gst" + | "sales_tax" + | "reverse_charge" + | "exempt" + | "not_subject" + | "custom"; + +export class TaxFamily { + private constructor(private readonly value: TaxFamilyType) {} + + public static create(value: string): Result { + const v = value?.toString() ?? ""; + const allowed: TaxFamilyType[] = [ + "iva", + "igic", + "ipsi", + "equivalence_surcharge", + "withholding", + "vat", + "gst", + "sales_tax", + "reverse_charge", + "exempt", + "not_subject", + "custom", + ]; + + if (!allowed.includes(v as TaxFamilyType)) { + return Result.fail(new InvalidTaxDefinitionFamilyError("Invalid tax family")); + } + + return Result.ok(new TaxFamily(v as TaxFamilyType)); + } + + public static fromPersistence(value: string): TaxFamily { + return new TaxFamily(value as TaxFamilyType); + } + + public toPrimitive(): TaxFamilyType { + return this.value; + } + + public toString(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-rate.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-rate.ts new file mode 100644 index 00000000..d86fa10f --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-rate.ts @@ -0,0 +1,40 @@ +import { Percentage, type PercentageProps, ValidationErrorCollection } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionRateError } from "./errors"; + +type TaxRateProps = PercentageProps; + +export class TaxRate extends Percentage { + static DEFAULT_SCALE = 2; + + static create({ value, scale }: TaxRateProps): Result { + if (scale && scale !== TaxRate.DEFAULT_SCALE) { + return Result.fail( + new ValidationErrorCollection("InvalidScale", [ + { message: `TaxRate scale must be ${TaxRate.DEFAULT_SCALE}` }, + ]) + ); + } + + // Basic range validation: 0.00 .. 100.00 (value expressed with scale 2 => 0 .. 10000) + if (value < 0) { + return Result.fail(new InvalidTaxDefinitionRateError("Tax rate must be >= 0")); + } + + if (value > 100 * 10 ** TaxRate.DEFAULT_SCALE) { + return Result.fail(new InvalidTaxDefinitionRateError("Tax rate must be <= 100.00")); + } + + return Result.ok( + new TaxRate({ + value, + scale: TaxRate.DEFAULT_SCALE, + }) + ); + } + + static zero() { + return TaxRate.create({ value: 0 }).data; + } +} diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-scope.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-scope.ts new file mode 100644 index 00000000..313fd60b --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-scope.ts @@ -0,0 +1,32 @@ +import { Result } from "@repo/rdx-utils"; + +import { InvalidTaxDefinitionScopeError } from "./errors"; + +export type TaxScopeType = "domestic" | "intra_eu" | "export" | "import" | "international"; + +export class TaxScope { + private constructor(private readonly value: TaxScopeType) {} + + public static create(value: string): Result { + const v = value?.toString() ?? ""; + const allowed: TaxScopeType[] = ["domestic", "intra_eu", "export", "import", "international"]; + + if (!allowed.includes(v as TaxScopeType)) { + return Result.fail(new InvalidTaxDefinitionScopeError("Invalid tax scope")); + } + + return Result.ok(new TaxScope(v as TaxScopeType)); + } + + public static fromPersistence(value: string): TaxScope { + return new TaxScope(value as TaxScopeType); + } + + public toPrimitive(): TaxScopeType { + return this.value; + } + + public toString(): string { + return this.value; + } +} diff --git a/modules/catalogs/src/api/index.ts b/modules/catalogs/src/api/index.ts index d0116c1b..72d49956 100644 --- a/modules/catalogs/src/api/index.ts +++ b/modules/catalogs/src/api/index.ts @@ -13,35 +13,62 @@ import { buildCatalogsPublicServices, } from "./infrastructure/di/catalogs.di"; -export * from "./infrastructure/payment-methods/persistence/sequelize"; +export * from "./application/services/catalog-public-services.interface"; // <- exportamos la interfaz de los servicios públicos para que otros módulos puedan usarla en sus dependencias + +//export * from "./infrastructure/payment-methods/persistence/sequelize"; <- ??? export const catalogsAPIModule: IModuleServer = { name: "catalogs", version: "1.0.0", dependencies: [], + /** + * Fase de SETUP + * ---------------- + * - Construye el dominio (una sola vez) + * - Define qué expone el módulo + * - NO conecta infraestructura + */ async setup(params) { + const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params; + + // 1) Dominio interno const internal = buildCatalogsDependencies(params); + + // 2) Servicios públicos (Application Services) const publicServices = buildCatalogsPublicServices(params, internal); - params.logger.info("🚀 Catalogs module dependencies registered", { + logger.info("🚀 Catalogs module dependencies registered", { label: this.name, }); return { + // Modelos Sequelize del módulo models: [...paymentMethodModels, ...paymentTermModels, ...taxRegimeModels], + + // Servicios expuestos a otros módulos services: { - paymentMethod: publicServices.paymentMethods, + paymentMethods: publicServices.paymentMethods, paymentTerms: publicServices.paymentTerms, taxRegimes: publicServices.taxRegimes, }, + + // Implementación privada del módulo internal, }; }, + /** + * Fase de START + * ------------- + * - Conecta el módulo al runtime + * - Puede usar servicios e internals ya construidos + * - NO construye dominio + */ async start(params) { const { logger } = params; + // Registro de rutas HTTP paymentMethodsRouter(params); paymentTermsRouter(params); taxRegimesRouter(params); @@ -50,6 +77,14 @@ export const catalogsAPIModule: IModuleServer = { label: this.name, }); }, + + /** + * Warmup opcional (si lo necesitas en el futuro) + * ---------------------------------------------- + * warmup(params) { + * ... + * } + */ }; export default catalogsAPIModule; diff --git a/modules/catalogs/src/api/infrastructure/index.ts b/modules/catalogs/src/api/infrastructure/index.ts index 23f8279d..1702efcf 100644 --- a/modules/catalogs/src/api/infrastructure/index.ts +++ b/modules/catalogs/src/api/infrastructure/index.ts @@ -1,3 +1,4 @@ export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-definitions"; export * from "./tax-regimes"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts new file mode 100644 index 00000000..6b086d9c --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/index.ts @@ -0,0 +1,2 @@ +export * from "./tax-definition-persistence-mappers.di"; +export * from "./tax-definition-repositories.di"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-persistence-mappers.di.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-persistence-mappers.di.ts new file mode 100644 index 00000000..d2e77649 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-persistence-mappers.di.ts @@ -0,0 +1,16 @@ +import { + SequelizeTaxDefinitionDomainMapper, + SequelizeTaxDefinitionSummaryMapper, +} from "../persistence"; + +export interface ITaxDefinitionPersistenceMappers { + domainMapper: SequelizeTaxDefinitionDomainMapper; + listMapper: SequelizeTaxDefinitionSummaryMapper; +} + +export const buildTaxDefinitionPersistenceMappers = (): ITaxDefinitionPersistenceMappers => { + const domainMapper = new SequelizeTaxDefinitionDomainMapper(); + const listMapper = new SequelizeTaxDefinitionSummaryMapper(); + + return { domainMapper, listMapper }; +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-repositories.di.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-repositories.di.ts new file mode 100644 index 00000000..e9dcc87a --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definition-repositories.di.ts @@ -0,0 +1,13 @@ +import type { Sequelize } from "sequelize"; + +import { SequelizeTaxDefinitionRepository } from "../persistence"; + +import type { ITaxDefinitionPersistenceMappers } from "./tax-definition-persistence-mappers.di"; + +export const buildTaxDefinitionRepository = (params: { + database: Sequelize; + mappers: ITaxDefinitionPersistenceMappers; +}) => { + const { database, mappers } = params; + return new SequelizeTaxDefinitionRepository(mappers.domainMapper, mappers.listMapper, database); +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts new file mode 100644 index 00000000..96e04610 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/index.ts @@ -0,0 +1,2 @@ +export * from "./di"; +export * from "./persistence"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/index.ts new file mode 100644 index 00000000..5c3a3a37 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/index.ts @@ -0,0 +1,7 @@ +export * from "./mappers"; +export * from "./models"; +export * from "./repositories"; + +import taxDefinitionModelInit from "./models/sequelize-tax-definition.model"; + +export const taxDefinitionModels = [taxDefinitionModelInit]; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..a114fb8f --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./sequelize-tax-definition-domain.mapper"; +export * from "./sequelize-tax-definition-summary.mapper"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts new file mode 100644 index 00000000..f7469a5e --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-domain.mapper.ts @@ -0,0 +1,157 @@ +import { SequelizeQueryMapper } from "@erp/core/api"; +import { + TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableResult, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { + TaxCalculationBehavior, + TaxDefinition, + TaxDefinitionCode as TaxDefinitionCodeVO, + TaxDefinitionName as TaxDefinitionNameVO, + TaxFamily, + TaxJurisdictionCountryCode, + TaxJurisdictionRegionCode, + TaxRate as TaxRateVO, + TaxScope, +} from "../../../../../domain"; +import type { TaxDefinitionModel } from "../models"; + +export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper< + TaxDefinitionModel, + TaxDefinition +> { + public mapToDomain(raw: TaxDefinitionModel): Result { + const errors: ValidationErrorDetail[] = []; + + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + const id = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const code = extractOrPushError(TaxDefinitionCodeVO.create(raw.code), "code", errors); + const name = extractOrPushError(TaxDefinitionNameVO.create(raw.name), "name", errors); + + const description = maybeFromNullableResult(raw.description, (v) => TextValue.create(v)); + + const rate = extractOrPushError( + TaxRateVO.create({ value: raw.rate_value, scale: raw.rate_scale }), + "rate", + errors + ); + + const taxFamily = extractOrPushError(TaxFamily.create(raw.tax_family), "tax_family", errors); + + const calculationBehavior = extractOrPushError( + TaxCalculationBehavior.create(raw.calculation_behavior), + "calculation_behavior", + errors + ); + + const jurisdictionCountryCode = extractOrPushError( + TaxJurisdictionCountryCode.create(raw.jurisdiction_country_code), + "jurisdiction_country_code", + errors + ); + + const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) => + TaxJurisdictionRegionCode.create(v) + ); + + const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors); + + const invoiceNote = maybeFromNullableResult(raw.invoice_note, (v) => TextValue.create(v)); + + const allowedSurchargeCodes = maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => { + if (!Array.isArray(v)) return Result.fail(new Error("Invalid allowed_surcharge_codes")); + + const arr: any[] = []; + for (const el of v) { + const r = TaxDefinitionCodeVO.create(String(el)); + if (r.isFailure) return Result.fail(new Error("Invalid allowed_surcharge_codes element")); + arr.push(r.getValue()); + } + + return Result.ok(arr); + }); + + // valid from / to + const validFrom = maybeFromNullableResult(raw.valid_from, (v) => Result.ok(String(v))); + const validTo = maybeFromNullableResult(raw.valid_to, (v) => Result.ok(String(v))); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("TaxDefinition mapping failed [mapToDomain]", errors) + ); + } + + const domainOrError = TaxDefinition.rehydrate( + { + companyId: companyId!, + code: code!, + name: name!, + description: description, + rate: rate!, + taxFamily: taxFamily!, + calculationBehavior: calculationBehavior!, + jurisdictionCountryCode: jurisdictionCountryCode!, + jurisdictionRegionCode: jurisdictionRegionCode, + taxScope: taxScope!, + invoiceNote: invoiceNote, + allowedSurchargeCodes: allowedSurchargeCodes, + isSystem: raw.is_system, + isActive: raw.is_active, + validFrom: validFrom, + validTo: validTo, + } as any, + id! + ); + + return Result.ok(domainOrError); + } + + public mapToPersistence(domain: TaxDefinition): Result, Error> { + const dto: Record = { + id: domain.id.toPrimitive(), + company_id: domain.companyId.toPrimitive(), + code: domain.code.toPrimitive(), + name: domain.name.toPrimitive(), + description: domain.description.match( + (v) => v.toPrimitive(), + () => null + ), + rate_value: domain.rate.toPrimitive(), + rate_scale: (domain.rate as any).scale ?? 2, + tax_family: domain.taxFamily.toPrimitive(), + calculation_behavior: domain.calculationBehavior.toPrimitive(), + jurisdiction_country_code: domain.jurisdictionCountryCode.toPrimitive(), + jurisdiction_region_code: domain.jurisdictionRegionCode.match( + (v) => v.toPrimitive(), + () => null + ), + tax_scope: domain.taxScope.toPrimitive(), + invoice_note: domain.invoiceNote.match( + (v) => v.toPrimitive(), + () => null + ), + allowed_surcharge_codes: domain.allowedSurchargeCodes.match( + (arr) => arr.map((c) => c.toPrimitive()), + () => null + ), + is_system: domain.isSystem, + is_active: domain.isActive, + valid_from: domain.validFrom.match( + (d) => d.toPrimitive(), + () => null + ), + valid_to: domain.validTo.match( + (d) => d.toPrimitive(), + () => null + ), + }; + + return Result.ok(dto); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-summary.mapper.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-summary.mapper.ts new file mode 100644 index 00000000..38f4c1ab --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/mappers/sequelize-tax-definition-summary.mapper.ts @@ -0,0 +1,48 @@ +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 { TaxDefinitionSummary } from "../../../../../application"; +import { TaxDefinitionCode } from "../../../../../domain"; +import type { TaxDefinitionModel } from "../models"; + +export class SequelizeTaxDefinitionSummaryMapper extends SequelizeQueryMapper< + TaxDefinitionModel, + TaxDefinitionSummary +> { + public mapToReadModel( + raw: TaxDefinitionModel, + _params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + const id = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const code = extractOrPushError(TaxDefinitionCode.create(raw.code), "code", errors); + const name = extractOrPushError(TextValue.create(raw.name), "name", errors); + + const isActive = raw.is_active; + const isSystem = raw.is_system; + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("TaxDefinition mapping failed [mapToDTO]", errors) + ); + } + + return Result.ok({ + id: id!, + companyId: companyId!, + code: code!, + name: name!, + isActive, + isSystem, + }); + } +} diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/sequelize-tax-definition.model.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/sequelize-tax-definition.model.ts new file mode 100644 index 00000000..9f8286a5 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/models/sequelize-tax-definition.model.ts @@ -0,0 +1,171 @@ +import { + type CreationOptional, + DataTypes, + type InferAttributes, + type InferCreationAttributes, + Model, + type Sequelize, +} from "sequelize"; + +export type TaxDefinitionCreationAttributes = InferCreationAttributes & {}; + +export class TaxDefinitionModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: string; + declare company_id: string; + declare code: string; + declare name: string; + declare description: CreationOptional; + + declare rate_value: number; + declare rate_scale: number; + + declare tax_family: string; + declare calculation_behavior: string; + declare jurisdiction_country_code: string; + declare jurisdiction_region_code: CreationOptional; + declare tax_scope: string; + + declare invoice_note: CreationOptional; + declare allowed_surcharge_codes: CreationOptional; + + declare is_system: boolean; + declare is_active: boolean; + + declare valid_from: CreationOptional; + declare valid_to: CreationOptional; + + static associate(_database: Sequelize) {} + + static hooks(_database: Sequelize) {} +} + +export default (database: Sequelize) => { + TaxDefinitionModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + company_id: { + type: DataTypes.UUID, + allowNull: false, + }, + code: { + type: DataTypes.STRING(40), + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + }, + + rate_value: { + type: DataTypes.SMALLINT, + allowNull: false, + }, + + rate_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 2, + }, + + tax_family: { + type: DataTypes.STRING(40), + allowNull: false, + }, + + calculation_behavior: { + type: DataTypes.STRING(40), + allowNull: false, + }, + + jurisdiction_country_code: { + type: DataTypes.STRING(8), + allowNull: false, + }, + + jurisdiction_region_code: { + type: DataTypes.STRING(16), + allowNull: true, + defaultValue: null, + }, + + tax_scope: { + type: DataTypes.STRING(40), + allowNull: false, + }, + + invoice_note: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + }, + + allowed_surcharge_codes: { + type: DataTypes.JSON, + allowNull: true, + defaultValue: null, + }, + + is_system: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + + is_active: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + + valid_from: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null, + }, + + valid_to: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null, + }, + }, + { + sequelize: database, + modelName: "TaxDefinitionModel", + tableName: "tax_definitions", + + underscored: true, + paranoid: true, + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { + name: "idx_tax_definitions_company_code", + fields: ["company_id", "code", "deleted_at"], + unique: true, + }, + ], + + whereMergeStrategy: "and", + defaultScope: {}, + scopes: {}, + } + ); + + return TaxDefinitionModel; +}; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/index.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..5ada17f3 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-tax-definition.repository"; diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts new file mode 100644 index 00000000..05d16ce6 --- /dev/null +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/persistence/sequelize/repositories/sequelize-tax-definition.repository.ts @@ -0,0 +1,174 @@ +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 { ITaxDefinitionRepository, TaxDefinitionSummary } from "../../../../../application"; +import type { TaxDefinition, TaxDefinitionCode } from "../../../../../domain"; +import type { + SequelizeTaxDefinitionDomainMapper, + SequelizeTaxDefinitionSummaryMapper, +} from "../mappers"; +import { TaxDefinitionModel } from "../models"; + +export class SequelizeTaxDefinitionRepository + extends SequelizeRepository + implements ITaxDefinitionRepository +{ + constructor( + private readonly domainMapper: SequelizeTaxDefinitionDomainMapper, + private readonly summaryMapper: SequelizeTaxDefinitionSummaryMapper, + database: Sequelize + ) { + super({ database }); + } + + async create( + taxDefinition: TaxDefinition, + transaction?: Transaction + ): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(taxDefinition); + if (dtoResult.isFailure) return Result.fail(dtoResult.error); + + await TaxDefinitionModel.create(dtoResult.data, { transaction }); + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + async update( + taxDefinition: TaxDefinition, + transaction?: Transaction + ): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(taxDefinition); + if (dtoResult.isFailure) return Result.fail(dtoResult.error); + + const { id, ...payload } = dtoResult.data as any; + const [affected] = await TaxDefinitionModel.update(payload, { + where: { id }, + transaction, + individualHooks: true, + }); + + if (affected === 0) { + return Result.fail( + new InfrastructureRepositoryError("Concurrency conflict or tax definition not found") + ); + } + + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + async existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const count = await TaxDefinitionModel.count({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(Boolean(count > 0)); + } catch (error: unknown) { + return Result.fail(translateSequelizeError(error)); + } + } + + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const row = await TaxDefinitionModel.findOne({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + if (!row) return Result.fail(new EntityNotFoundError("TaxDefinition", "id", id.toString())); + + return this.domainMapper.mapToDomain(row); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + async getByCodeInCompany( + companyId: UniqueID, + code: TaxDefinitionCode, + transaction?: Transaction + ): Promise> { + try { + const row = await TaxDefinitionModel.findOne({ + where: { code: code.toString(), company_id: companyId.toString() }, + transaction, + }); + if (!row) + return Result.fail(new EntityNotFoundError("TaxDefinition", "code", code.toString())); + return this.domainMapper.mapToDomain(row); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + async findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + try { + const criteriaConverter = new CriteriaToSequelizeConverter(); + const query = criteriaConverter.convert(criteria, { + mappings: { + isActive: "is_active", + }, + searchableFields: ["code", "name", "description", "invoice_note"], + sortableFields: ["code", "name"], + enableFullText: true, + database: this.database, + strictMode: true, + }); + + query.where = { ...query.where, company_id: companyId.toString(), deleted_at: null }; + + const [rows, count] = await Promise.all([ + TaxDefinitionModel.findAll({ ...query, transaction }), + TaxDefinitionModel.count({ where: query.where, distinct: true, transaction }), + ]); + + return this.summaryMapper.mapToReadModelCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + async deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction + ): Promise> { + try { + const deleted = await TaxDefinitionModel.destroy({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + if (deleted === 0) + return Result.fail(new EntityNotFoundError("TaxDefinition", "id", id.toString())); + return Result.ok(true); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } +} diff --git a/modules/catalogs/src/web/index.ts b/modules/catalogs/src/web/index.ts index 2666407f..74e10b7c 100644 --- a/modules/catalogs/src/web/index.ts +++ b/modules/catalogs/src/web/index.ts @@ -4,3 +4,4 @@ export { } from "./manifest"; export * from "./payment-methods"; export * from "./payment-terms"; +export * from "./tax-regimes"; diff --git a/modules/catalogs/src/web/manifest.ts b/modules/catalogs/src/web/manifest.ts index 4f1160d7..6fa23e74 100644 --- a/modules/catalogs/src/web/manifest.ts +++ b/modules/catalogs/src/web/manifest.ts @@ -8,7 +8,7 @@ export const CatalogsModuleManifest: IModuleClient = { version: MODULE_VERSION, dependencies: ["auth", "Core"], protected: true, - layout: "app", + layout: "app-sidebar", routes: (params: ModuleClientParams) => { return []; diff --git a/modules/catalogs/src/web/tax-regimes/index.ts b/modules/catalogs/src/web/tax-regimes/index.ts new file mode 100644 index 00000000..db220ed0 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/index.ts @@ -0,0 +1,2 @@ +export * from "./shared"; +export * from "./utils"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts b/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts new file mode 100644 index 00000000..7d8bb369 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts @@ -0,0 +1 @@ +export * from "./list-tax-regimes.adapter"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts b/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts new file mode 100644 index 00000000..d15db0c3 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts @@ -0,0 +1,54 @@ +import type { ListTaxRegimesResponseDTO } from "../../../../common"; +import type { ListTaxRegimesResult } from "../api"; +import type { TaxRegimeList, TaxRegimeListRow } from "../entities"; + +/** + * Adaptador para transformar los datos de la API de ListTaxRegimesResult + * a la entidad TaxRegimeList utilizada en la aplicación. + * Reglas de adaptación: + * - page, per_page, total_pages, total_items se asignan directamente. + * - items se transforma utilizando TaxRegimeListRowAdapter para cada elemento. + * + * @param pageDto - lista de proformas desde la API. + * @param context - Contexto adicional opcional para la adaptación. + * @returns {TaxRegimeList} Objeto adaptado a PaymentMehodList. + */ + +export const ListTaxRegimesAdapter = { + fromDto(dto: ListTaxRegimesResult, context?: unknown): TaxRegimeList { + return { + page: dto.page, + perPage: dto.per_page, + totalPages: dto.total_pages, + totalItems: dto.total_items, + items: dto.items.map((rowDto) => TaxRegimeListRowAdapter.fromDto(rowDto, context)), + }; + }, +}; + +/** + * Adaptador para transformar los items de la API de ListPaymentMehodsResult a la entidad PaymentMehodListRow. + * Reglas de adaptación: + * - id, company_id se asignan directamente. + * + * @param rowDto - item de proforma desde la API. + * @param context - Contexto adicional opcional para la adaptación. + * @returns {TaxRegimeListRow} Objeto adaptado a PaymentMehodListRow. + */ + +type ListTaxRegimesItemOutput = ListTaxRegimesResponseDTO["items"][number]; + +const TaxRegimeListRowAdapter = { + fromDto(dto: ListTaxRegimesItemOutput, context?: unknown): TaxRegimeListRow { + return { + id: dto.id, + companyId: dto.company_id, + + code: dto.code, + description: dto.description, + + isSystem: dto.is_system, + isActive: dto.is_active, + }; + }, +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/index.ts b/modules/catalogs/src/web/tax-regimes/shared/api/index.ts new file mode 100644 index 00000000..427e1e45 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/index.ts @@ -0,0 +1 @@ +export * from "./list-tax-regimes-by-criteria.api"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/list-tax-regimes-by-criteria.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/list-tax-regimes-by-criteria.api.ts new file mode 100644 index 00000000..f35cfea0 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/list-tax-regimes-by-criteria.api.ts @@ -0,0 +1,38 @@ +import type { CriteriaDTO } from "@erp/core"; +import type { IDataSource } from "@erp/core/client"; + +import type { ListTaxRegimesResponseDTO } from "../../../../common"; + +/** + * Recupera una lista de regímenes fiscales del sistema utilizando la + * fuente de datos proporcionada y los criterios de búsqueda especificados. + * + * @param dataSource - La fuente de datos para interactuar con la API. + * @param params - Los parámetros necesarios para listar los regímenes fiscales, incluyendo los criterios de búsqueda. + * @returns Una promesa que resuelve con una lista de regímenes fiscales que cumplen con los criterios especificados. + * @throws Error si la recuperación de la lista de regímenes fiscales falla. + */ + +export type ListTaxRegimesByCriteriaParams = { + criteria?: CriteriaDTO; + signal?: AbortSignal; +}; + +export type ListTaxRegimesResult = ListTaxRegimesResponseDTO; + +export function getListTaxRegimesByCriteria( + dataSource: IDataSource, + params: ListTaxRegimesByCriteriaParams +): Promise { + const { criteria, signal } = params || { + criteria: { + page: 1, + per_page: 9999, + }, + signal: undefined, + }; + return dataSource.getList("catalogs/tax-regimes", { + signal, + ...criteria, + }); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/entities/index.ts b/modules/catalogs/src/web/tax-regimes/shared/entities/index.ts new file mode 100644 index 00000000..68f216f7 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./tax-regime.entity"; +export * from "./tax-regime-list.entity"; +export * from "./tax-regime-list-row.entity"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list-row.entity.ts b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list-row.entity.ts new file mode 100644 index 00000000..0521ba70 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list-row.entity.ts @@ -0,0 +1,17 @@ +/** + * Interface que representa una fila de la lista de + * regímenes fiscales en el sistema, adaptada desde la respuesta de la API. + * Contiene los campos justos para mostrar + * la información básica de cada régimen fiscal en la lista. + */ + +export interface TaxRegimeListRow { + id: string; + companyId: string; + + code: string; + description: string; + + isSystem: boolean; + isActive: boolean; +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list.entity.ts b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list.entity.ts new file mode 100644 index 00000000..5b3b0fce --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime-list.entity.ts @@ -0,0 +1,14 @@ +import type { TaxRegimeListRow } from "./tax-regime-list-row.entity"; + +/** + * Interface que representa la respuesta paginada de una lista de regímenes fiscales, + * adaptada desde la respuesta de la API. + */ + +export interface TaxRegimeList { + items: TaxRegimeListRow[]; + totalPages: number; + totalItems: number; + page: number; + perPage: number; +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime.entity.ts b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime.entity.ts new file mode 100644 index 00000000..03f67a4c --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/entities/tax-regime.entity.ts @@ -0,0 +1,10 @@ +export interface TaxRegime { + id: string; + companyId: string; + + code: string; + description: string; + + isSystem: boolean; + isActive: boolean; +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts new file mode 100644 index 00000000..6c7a9979 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-tax-regime-list-query"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts new file mode 100644 index 00000000..8b57fab8 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts @@ -0,0 +1,44 @@ +import type { QueryKey } from "@tanstack/react-query"; + +import type { ListTaxRegimesRequestDTO } from "../../../../common"; + +/** + * Prefijo base para listados + */ +export const LIST_TAX_REGIMES_QUERY_KEY_PREFIX = ["tax-regimes"] as const; + +/** + * Query key para listado de tax regimes + */ +export const LIST_TAX_REGIMES_QUERY_KEY = (criteria?: ListTaxRegimesRequestDTO): QueryKey => + [ + ...LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + { + pageNumber: criteria?.pageNumber ?? 1, + pageSize: criteria?.pageSize ?? 5, + q: criteria?.q ?? "", + filters: criteria?.filters ?? [], + orderBy: criteria?.orderBy ?? "", + order: criteria?.order ?? "", + }, + ] as const; + +/** + * Query key para detalle de tax regime + */ +export const TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX = ["tax-regimes:detail"] as const; +export const TAX_REGIME_QUERY_KEY = (taxRegimeId?: string): QueryKey => [ + ...TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX, + { taxRegimeId }, +]; + +/** + * Keys para mutaciones + */ +export const CREATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:create"] as const; +export const UPDATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:update"] as const; +export const DELETE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:delete"] as const; + +/** + * Operaciones de dominio + */ diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-list-query.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-list-query.ts new file mode 100644 index 00000000..c4ba2d4b --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-list-query.ts @@ -0,0 +1,32 @@ +import type { CriteriaDTO } from "@erp/core"; +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { ListTaxRegimesAdapter } from "../adapters"; +import { getListTaxRegimesByCriteria } from "../api"; +import type { TaxRegimeList } from "../entities"; + +import { LIST_TAX_REGIMES_QUERY_KEY } from "./keys"; + +export interface TaxRegimesListQueryOptions { + enabled?: boolean; + criteria?: Partial; +} + +export const useTaxRegimesListQuery = ( + options?: TaxRegimesListQueryOptions +): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = options?.enabled ?? true; + const criteria = options?.criteria ?? {}; + + return useQuery({ + queryKey: LIST_TAX_REGIMES_QUERY_KEY(criteria), + queryFn: async ({ signal }) => { + const dto = await getListTaxRegimesByCriteria(dataSource, { signal, criteria }); + return ListTaxRegimesAdapter.fromDto(dto); + }, + enabled, + placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/index.ts b/modules/catalogs/src/web/tax-regimes/shared/index.ts new file mode 100644 index 00000000..3a4614c9 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/index.ts @@ -0,0 +1,2 @@ +export * from "./entities"; +export * from "./hooks"; diff --git a/modules/catalogs/src/web/tax-regimes/utils/index.ts b/modules/catalogs/src/web/tax-regimes/utils/index.ts new file mode 100644 index 00000000..ac1acc16 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/utils/index.ts @@ -0,0 +1 @@ +export * from "./tax-regime-options.utils"; diff --git a/modules/catalogs/src/web/tax-regimes/utils/tax-regime-options.utils.ts b/modules/catalogs/src/web/tax-regimes/utils/tax-regime-options.utils.ts new file mode 100644 index 00000000..613313aa --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/utils/tax-regime-options.utils.ts @@ -0,0 +1,10 @@ +import type { SelectFieldItem } from "@repo/rdx-ui/components"; + +import type { TaxRegimeListRow } from "../shared"; + +export const getTaxRegimeOptions = (taxRegimes: TaxRegimeListRow[]): SelectFieldItem[] => { + return taxRegimes.map((taxRegime) => ({ + value: taxRegime.id, + label: taxRegime.description, + })); +}; diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts index 0ac9eb1a..a0e365d0 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts @@ -180,6 +180,14 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { ); }); + toPatchField(dto.tax_regime_code).ifSet((taxRegimeCode) => { + proformaPatchProps.taxRegimeCode = extractOrPushError( + maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))), + "tax_regime_code", + errors + ); + }); + if (dto.items !== undefined) { proformaPatchProps.items = this.mapItemsProps(dto.items, { errors }); } diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts index 1dc5b980..481f9110 100644 --- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts @@ -53,6 +53,7 @@ export interface IProformaCreateProps { linkedInvoiceId: Maybe; paymentMethodId: Maybe; + taxRegimeCode: Maybe; items: IProformaItemCreateProps[]; globalDiscountPercentage: DiscountPercentage; @@ -100,6 +101,7 @@ export interface IProforma { currencyCode: CurrencyCode; paymentMethodId: Maybe; + taxRegimeCode: Maybe; linkedInvoiceId: Maybe; @@ -262,6 +264,10 @@ export class Proforma extends AggregateRoot implements IP return this.props.paymentMethodId; } + public get taxRegimeCode(): Maybe { + return this.props.taxRegimeCode; + } + public get linkedInvoiceId(): Maybe { return this.props.linkedInvoiceId; } @@ -290,6 +296,10 @@ export class Proforma extends AggregateRoot implements IP return this.paymentMethodId.isSome(); } + public get hasTaxRegime() { + return this.taxRegimeCode.isSome(); + } + public issue(): Result { // Antes de cambiar el estado de la proforma, // comprobamos que se cumplen las condiciones diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts index 225424c8..ed1d01fb 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts @@ -64,6 +64,10 @@ export class CustomerInvoiceModel extends Model< declare payment_method_id: CreationOptional; declare payment_method_description: CreationOptional; + // Tax regime + declare tax_regime_code: CreationOptional; + declare tax_regime_description: CreationOptional; + // Subtotal declare subtotal_amount_value: number; declare subtotal_amount_scale: number; @@ -310,6 +314,16 @@ export default (database: Sequelize) => { defaultValue: null, }, + tax_regime_code: { + type: DataTypes.STRING(), + allowNull: true, + }, + + tax_regime_description: { + type: new DataTypes.STRING(), + allowNull: true, + }, + subtotal_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index 92f5474a..b93aefe1 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -1,4 +1,5 @@ -import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api"; +import type { ICatalogPublicServices } from "@erp/catalogs/api"; +import { type ModuleParams, buildTransactionManager } from "@erp/core/api"; import { type ChangeStatusProformaUseCase, @@ -50,11 +51,11 @@ export type ProformasInternalDeps = { }; export function buildProformasDependencies(params: ModuleParams): ProformasInternalDeps { - const { database } = params; + const { database, getService } = params; + const catalogs = getService("catalogs"); // Infrastructure const transactionManager = buildTransactionManager(database); - const catalogs = buildCatalogs(); const persistenceMappers = buildProformaPersistenceMappers(catalogs); const repository = buildProformaRepository({ database, mappers: persistenceMappers }); diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.bak b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.bak deleted file mode 100644 index cad11824..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.bak +++ /dev/null @@ -1,260 +0,0 @@ -import type { JsonTaxCatalogProvider } from "@erp/core"; -import { DiscountPercentage } from "@erp/core/api"; -import { - CurrencyCode, - DomainError, - LanguageCode, - Percentage, - TextValue, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common"; -import { - type IProformaItemCreateProps, - InvoiceNumber, - InvoicePaymentMethod, - type InvoiceRecipient, - InvoiceSerie, - InvoiceStatus, - IssuedInvoiceItem, - type IssuedInvoiceItemProps, - ItemAmount, - ItemDescription, - ItemQuantity, - type ProformaCreateProps, -} from "../../../../domain"; - -/** - * CreateProformaPropsMapper - * Convierte el DTO a las props validadas (CustomerProps). - * No construye directamente el agregado. - * - * @param dto - DTO con los datos de la factura de cliente - * @returns - - * - */ - -export class CreateProformaRequestMapper { - private readonly taxCatalog: JsonTaxCatalogProvider; - private errors: ValidationErrorDetail[] = []; - private languageCode?: LanguageCode; - private currencyCode?: CurrencyCode; - - constructor(params: { taxCatalog: JsonTaxCatalogProvider }) { - this.taxCatalog = params.taxCatalog; - this.errors = []; - } - - public map(dto: CreateProformaRequestDTO, params: { companyId: UniqueID }) { - const { companyId } = params; - try { - this.errors = []; - - const defaultStatus = InvoiceStatus.draft(); - - const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); - - const customerId = extractOrPushError( - UniqueID.create(dto.customer_id), - "customer_id", - this.errors - ); - - const recipient = Maybe.none(); - - const proformaNumber = extractOrPushError( - InvoiceNumber.create(dto.invoice_number), - "invoice_number", - this.errors - ); - - const series = extractOrPushError( - maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)), - "series", - this.errors - ); - - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(dto.invoice_date), - "invoice_date", - this.errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - this.errors - ); - - const reference = extractOrPushError( - maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))), - "reference", - this.errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))), - "description", - this.errors - ); - - const notes = extractOrPushError( - maybeFromNullableResult(dto.notes, (value) => TextValue.create(value)), - "notes", - this.errors - ); - - this.languageCode = extractOrPushError( - LanguageCode.create(dto.language_code), - "language_code", - this.errors - ); - - this.currencyCode = extractOrPushError( - CurrencyCode.create(dto.currency_code), - "currency_code", - this.errors - ); - - const paymentMethod = extractOrPushError( - maybeFromNullableResult(dto.payment_method, (value) => - InvoicePaymentMethod.create({ paymentDescription: value }) - ), - "payment_method", - this.errors - ); - - const globalDiscountPercentage = extractOrPushError( - Percentage.create({ - value: Number(dto.global_discount_percentage.value), - scale: Number(dto.global_discount_percentage.scale), - }), - "discount_percentage", - this.errors - ); - - const items = this.mapItems(dto.items); - - if (this.errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice props mapping failed", this.errors) - ); - } - - const proformaProps: Omit & { items: IProformaItemCreateProps[] } = { - companyId, - status: defaultStatus!, - - invoiceNumber: proformaNumber!, - series: series!, - - invoiceDate: invoiceDate!, - operationDate: operationDate!, - - customerId: customerId!, - recipient: recipient!, - - reference: reference!, - description: description!, - notes: notes!, - - languageCode: this.languageCode!, - currencyCode: this.currencyCode!, - - paymentMethod: paymentMethod!, - - globalDiscountPercentage: globalDiscountPercentage!, - - items: - }; - - return Result.ok({ id: proformaId!, props: proformaProps }); - } catch (err: unknown) { - return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err })); - } - } - - private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemCreateProps[] { - const proformaItems = CustomerInvoiceItems.create({ - currencyCode: this.currencyCode!, - languageCode: this.languageCode!, - items: [], - }); - - items.forEach((item, index) => { - const description = extractOrPushError( - maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)), - "description", - this.errors - ); - - const quantity = extractOrPushError( - maybeFromNullableResult(item.quantity, (value) => ItemQuantity.create(value)), - "quantity", - this.errors - ); - - const unitAmount = extractOrPushError( - maybeFromNullableResult(item.unit_amount, (value) => ItemAmount.create(value)), - "unit_amount", - this.errors - ); - - const discountPercentage = extractOrPushError( - maybeFromNullableResult(item.item_discount_percentage, (value) => - DiscountPercentage.create(value) - ), - "discount_percentage", - this.errors - ); - - const taxes = this.mapTaxes(item, index); - - const itemProps: IssuedInvoiceItemProps = { - currencyCode: this.currencyCode!, - languageCode: this.languageCode!, - description: description!, - quantity: quantity!, - unitAmount: unitAmount!, - itemDiscountPercentage: discountPercentage!, - taxes: taxes, - }; - - const itemResult = IssuedInvoiceItem.create(itemProps); - if (itemResult.isSuccess) { - proformaItems.add(itemResult.data); - } else { - this.errors.push({ - path: `items[${index}]`, - message: itemResult.error.message, - }); - } - }); - return proformaItems; - } - - private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) { - const taxes = ItemTaxes.create([]); - - item.taxes.split(",").forEach((tax_code, taxIndex) => { - const taxResult = Tax.createFromCode(tax_code, this.taxCatalog); - if (taxResult.isSuccess) { - taxes.add(taxResult.data); - } else { - this.errors.push({ - path: `items[${itemIndex}].taxes[${taxIndex}]`, - message: taxResult.error.message, - }); - } - }); - return taxes; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index d9a305d5..5e4e164d 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -1,4 +1,5 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; +import type { ICatalogPublicServices } from "@erp/catalogs/api"; import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api"; import { type NextFunction, type Request, type Response, Router } from "express"; @@ -34,9 +35,11 @@ export const proformasRouter = (params: StartParams) => { const deps = getInternal("customer-invoices", "proformas"); const issuedInvoicesServices = getService("self:issuedInvoices"); + const catalogServices = getService("self:catalogs"); const publicServices = { issuedInvoiceServices: issuedInvoicesServices, + catalogServices: catalogServices, }; const router: Router = Router({ mergeParams: true }); diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts index 1cfbf7f9..29216252 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts @@ -1,4 +1,4 @@ -import type { JsonPaymentCatalogProvider } from "@erp/core"; +import type { IPaymentMethodPublicServices, ITaxRegimePublicServices } from "@erp/catalogs/api"; import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { CurrencyCode, @@ -40,18 +40,21 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< private _recipientMapper: SequelizeProformaRecipientDomainMapper; private _taxesMapper: SequelizeProformaTaxesDomainMapper; - private _paymentCatalog: JsonPaymentCatalogProvider; + private _paymentMethodCatalog: IPaymentMethodPublicServices; + private _taxRegimeCatalog: ITaxRegimePublicServices; constructor(params: MapperParamsType) { super(); - const { paymentCatalog } = params as { - paymentCatalog: JsonPaymentCatalogProvider; + const { paymentCatalog, taxRegimeCatalog } = params as { + paymentCatalog: IPaymentMethodPublicServices; + taxRegimeCatalog: ITaxRegimePublicServices; }; - this._paymentCatalog = paymentCatalog; + this._paymentMethodCatalog = paymentCatalog; + this._taxRegimeCatalog = taxRegimeCatalog; - if (!this._paymentCatalog) { + if (!this._paymentMethodCatalog) { throw new Error('paymentCatalog not defined ("SequelizeProformaDomainMapper")'); } @@ -168,6 +171,13 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< errors ); + // Tax regime code + const taxRegimeCode = extractOrPushError( + maybeFromNullableResult(raw.tax_regime_code, (value) => Result.ok(String(value))), + "tax_regime_code", + errors + ); + // % descuento global (VO) const globalDiscountPercentage = extractOrPushError( DiscountPercentage.create({ @@ -198,6 +208,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< languageCode, currencyCode, paymentMethodId, + taxRegimeCode, globalDiscountPercentage, linkedInvoiceId, @@ -267,6 +278,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< paymentMethodId: attributes.paymentMethodId!, + taxRegimeCode: attributes.taxRegimeCode!, + linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia. }; @@ -328,7 +341,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< if (source.hasPaymentMethod) { const paymentId = source.paymentMethodId.unwrap(); - const paymentOrNot = this._paymentCatalog.findById(paymentId.toString()); + const paymentOrNot = this._paymentMethodCatalog.findById(paymentId.toString()); if (paymentOrNot.isSome()) { const paymentItem = paymentOrNot.unwrap(); @@ -340,7 +353,27 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< } } - // 5) Si hubo errores de mapeo, devolvemos colección de validación + // 5) Tax regime + let taxRegime: { + code: string | null; + description: string | null; + } = { code: null, description: null }; + + if (source.hasTaxRegime) { + const taxRegimeCode = source.taxRegimeCode.unwrap(); + const taxRegimeOrNot = this._taxRegimeCatalog.findBy(taxRegimeCode.toString()); + + if (taxRegimeOrNot.isSome()) { + const taxRegimeItem = taxRegimeOrNot.unwrap(); + + taxRegime = { + code: taxRegimeItem.code ?? null, + description: taxRegimeItem.description ?? null, + }; + } + } + + // 6) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) @@ -376,6 +409,9 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< payment_method_id: payment.id, payment_method_description: payment.description, + tax_regime_code: taxRegime.code, + tax_regime_description: taxRegime.description, + subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, diff --git a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts index 74b80fc5..c1ef6042 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts @@ -53,6 +53,7 @@ export const UpdateProformaByIdRequestSchema = z.object({ payment_method_id: z.uuid().nullable().optional(), payment_term_id: z.uuid().nullable().optional(), + tax_regime_code: z.string().nullable().optional(), // retención como código??? retencion_15 diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index 9b798669..7d98278a 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -13,6 +13,7 @@ import { IssuedInvoiceRecipientSummarySchema, IssuedInvoiceStatusSchema, PaymentMethodRefSchema, + TaxRegimeRefSchema, TaxesBreakdownSchema, VerifactuRecordSchema, } from "../../shared"; @@ -45,6 +46,8 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ payment_method: PaymentMethodRefSchema.nullable(), + tax_regime: TaxRegimeRefSchema.nullable(), + subtotal_amount: MoneySchema, items_discount_amount: MoneySchema, diff --git a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts index 4f27d45f..e7856dbf 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts @@ -8,7 +8,12 @@ import { } from "@erp/core"; import { z } from "zod/v4"; -import { PaymentMethodRefSchema, PaymentTermRefSchema, TaxesBreakdownSchema } from "../../shared"; +import { + PaymentMethodRefSchema, + PaymentTermRefSchema, + TaxRegimeRefSchema, + TaxesBreakdownSchema, +} from "../../shared"; import { ProformaItemDetailSchema, ProformaRecipientSummarySchema, @@ -43,6 +48,8 @@ export const GetProformaByIdResponseSchema = z.object({ payment_method: PaymentMethodRefSchema.nullable(), payment_term: PaymentTermRefSchema.nullable(), + tax_regime: TaxRegimeRefSchema.nullable(), + subtotal_amount: MoneySchema, items_discount_amount: MoneySchema, global_discount_percentage: PercentageSchema, diff --git a/modules/customer-invoices/src/common/dto/shared/index.ts b/modules/customer-invoices/src/common/dto/shared/index.ts index a2fe0e3b..33adbd78 100644 --- a/modules/customer-invoices/src/common/dto/shared/index.ts +++ b/modules/customer-invoices/src/common/dto/shared/index.ts @@ -4,4 +4,5 @@ export * from "./payment-method-ref.dto"; export * from "./payment-term-ref.dto"; export * from "./proforma"; export * from "./tax-combination-code.dto"; +export * from "./tax-regime-ref.dto"; export * from "./taxes-breakdown.dto"; diff --git a/modules/customer-invoices/src/common/dto/shared/tax-regime-ref.dto.ts b/modules/customer-invoices/src/common/dto/shared/tax-regime-ref.dto.ts new file mode 100644 index 00000000..f9d3b444 --- /dev/null +++ b/modules/customer-invoices/src/common/dto/shared/tax-regime-ref.dto.ts @@ -0,0 +1,8 @@ +import { z } from "zod/v4"; + +export const TaxRegimeRefSchema = z.object({ + code: z.string(), + description: z.string(), +}); + +export type TaxRegimeRefDTO = z.infer; diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts index 34391391..60eaaa6a 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts @@ -46,6 +46,8 @@ export const GetProformaByIdAdapter = { paymentMethodId: dto.payment_method?.id ?? null, paymentTermId: dto.payment_term?.id ?? null, + taxRegimeCode: dto.tax_regime?.code ?? null, + subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount), itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount), diff --git a/modules/customer-invoices/src/web/proformas/shared/entities/proforma.entity.ts b/modules/customer-invoices/src/web/proformas/shared/entities/proforma.entity.ts index 47783818..394433bb 100644 --- a/modules/customer-invoices/src/web/proformas/shared/entities/proforma.entity.ts +++ b/modules/customer-invoices/src/web/proformas/shared/entities/proforma.entity.ts @@ -31,6 +31,7 @@ export interface Proforma { customerId: string; recipient: ProformaRecipient; + taxRegimeCode: string | null; taxes: ProformaTaxSummary[]; paymentMethodId: string | null; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts index 581e5ece..9016ab56 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts @@ -39,7 +39,7 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd proforma.globalDiscountPercentage ?? proformaDefaults.globalDiscountPercentage, taxMode: fiscalDefaults.taxMode, - taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API + taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, hasTaxPercentage: fiscalDefaults.taxMode === "single" && fiscalDefaults.defaultTaxPercentage !== null, diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts index 9adc9d1d..8c3f4d43 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useRef } from "react"; +import { getTaxRegimeOptions, useTaxRegimesListQuery } from "@erp/catalogs/client/tax-regimes"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { type UseFormReturn, useWatch } from "react-hook-form"; import { @@ -6,34 +7,15 @@ import { type ProformaTaxPercentageOption, getProformaRecPercentage, } from "../../shared"; -import type { ProformaTaxMode, ProformaUpdateForm } from "../entities"; +import type { ProformaUpdateForm } from "../entities"; interface UseUpdateProformaTaxControllerParams { form: UseFormReturn; } -export interface UseUpdateProformaTaxControllerResult { - taxMode: ProformaTaxMode; - - hasTaxPercentage: boolean; - taxPercentage: number | null; - - hasRecPercentage: boolean; - recPercentage: number | null; - - hasRetentionPercentage: boolean; - retentionPercentage: number | null; - - usesSingleTax: boolean; - usesPerLineTax: boolean; - - enablePerLineTaxes: () => void; - disablePerLineTaxes: () => void; - - updateTaxPercentage: (newTaxPercentage: ProformaTaxPercentageOption) => void; - updateRecPercentage: (enabled: boolean) => void; - updateRetentionPercentage: (newRetentionPercentage: ProformaRetentionPercentageOption) => void; -} +export type UseUpdateProformaTaxControllerResult = ReturnType< + typeof useUpdateProformaTaxController +>; const resolveRecPercentage = ( enabled: boolean, @@ -46,9 +28,7 @@ const resolveRecPercentage = ( return getProformaRecPercentage(taxPercentage as ProformaTaxPercentageOption); }; -export const useUpdateProformaTaxController = ({ - form, -}: UseUpdateProformaTaxControllerParams): UseUpdateProformaTaxControllerResult => { +export const useUpdateProformaTaxController = ({ form }: UseUpdateProformaTaxControllerParams) => { const { control, getValues, setValue } = form; const taxMode = useWatch({ control, name: "taxMode" }); @@ -64,6 +44,18 @@ export const useUpdateProformaTaxController = ({ const hasMountedRef = useRef(false); + const taxRegimesQuery = useTaxRegimesListQuery({ + criteria: { + filters: [ + { + field: "isActive", + operator: "EQUALS", + value: "true", + }, + ], + }, + }); + useEffect(() => { if (taxMode !== "single") return; @@ -180,6 +172,10 @@ export const useUpdateProformaTaxController = ({ [setValue] ); + const taxRegimeOptions = useMemo(() => { + return getTaxRegimeOptions(taxRegimesQuery.data?.items ?? []); + }, [taxRegimesQuery.data?.items]); + return { taxMode, @@ -201,5 +197,7 @@ export const useUpdateProformaTaxController = ({ updateTaxPercentage, updateRecPercentage, updateRetentionPercentage, + + taxRegimeOptions, }; }; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-patch.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-patch.entity.ts index f6615522..5ff80a4f 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-patch.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-patch.entity.ts @@ -15,7 +15,6 @@ export interface ProformaItemUpdatePatch { id: string; position: number; - isValued: boolean; description: string | null; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx index 187ee255..88df8b96 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx @@ -1,7 +1,6 @@ // modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx import type { CustomerSelectionOption } from "@erp/customers"; -import { PercentageField } from "@repo/rdx-ui/components"; import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; import { cn } from "@repo/shadcn-ui/lib/utils"; @@ -14,7 +13,6 @@ import type { UseUpdateProformaTaxControllerResult, UseUpdateProformaTotalsControllerResult, } from "../../controllers"; -import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary"; import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor"; import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor"; @@ -89,24 +87,10 @@ export const ProformaUpdateEditorForm = ({ selectedCustomer={selectedCustomer} /> - - - - } - layout="vertical" - showRec={taxCtrl.hasRecPercentage} - showRetention={taxCtrl.hasRetentionPercentage} - totals={totalsCtrl.totals} +