diff --git a/modules/catalogs/src/api/application/tax-definitions/__tests__/tax-definition-test-support.ts b/modules/catalogs/src/api/application/tax-definitions/__tests__/tax-definition-test-support.ts new file mode 100644 index 00000000..0d6abdd4 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/__tests__/tax-definition-test-support.ts @@ -0,0 +1,144 @@ +import { EntityNotFoundError } from "@erp/core/api"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import { CountryCode, CountryRegionCode, TextValue, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { Collection, Maybe, Result } from "@repo/rdx-utils"; + +import { + TaxCalculationBehavior, + TaxDefinition, + TaxDefinitionCode, + TaxDefinitionName, + TaxFamily, + TaxRate, + TaxScope, +} from "../../../domain"; +import type { TaxDefinitionSummary } from "../models"; +import type { ITaxDefinitionRepository } from "../repositories"; + +const valueOf = (result: Result): T => { + if (result.isFailure) throw result.error; + return result.data; +}; + +export const buildTaxDefinition = (params?: { + companyId?: UniqueID; + id?: UniqueID; + isActive?: boolean; + isSystem?: boolean; + allowedSurchargeCodes?: string[] | null; +}) => { + const companyId = params?.companyId ?? UniqueID.generateNewID(); + const allowedSurchargeCodes = params?.allowedSurchargeCodes; + + return valueOf( + TaxDefinition.create( + { + companyId, + code: valueOf(TaxDefinitionCode.create("iva_21")), + name: valueOf(TaxDefinitionName.create("IVA general")), + description: Maybe.some(valueOf(TextValue.create("IVA aplicable"))), + rate: valueOf(TaxRate.create({ value: 2100, scale: 2 })), + taxFamily: valueOf(TaxFamily.create("iva")), + calculationBehavior: valueOf(TaxCalculationBehavior.create("additive")), + jurisdictionCountryCode: valueOf(CountryCode.create("ES")), + jurisdictionRegionCode: Maybe.some(valueOf(CountryRegionCode.create("ES-M"))), + taxScope: valueOf(TaxScope.create("domestic")), + invoiceNote: Maybe.some(valueOf(TextValue.create("Factura con IVA"))), + allowedSurchargeCodes: + allowedSurchargeCodes === null + ? Maybe.none() + : Maybe.some( + (allowedSurchargeCodes ?? ["rec_5_2", "rec_1_4"]).map((code) => + valueOf(TaxDefinitionCode.create(code)) + ) + ), + isSystem: params?.isSystem ?? false, + isActive: params?.isActive ?? true, + validFrom: Maybe.some(valueOf(UtcDate.createFromISO("2026-01-01"))), + validTo: Maybe.some(valueOf(UtcDate.createFromISO("2026-12-31"))), + }, + params?.id ?? UniqueID.generateNewID() + ) + ); +}; + +export class InMemoryTaxDefinitionRepository implements ITaxDefinitionRepository { + private readonly items = new Map(); + private readonly deleted = new Set(); + public updateCalls = 0; + public deleteCalls = 0; + + async create(taxDefinition: TaxDefinition): Promise> { + this.items.set(taxDefinition.id.toString(), taxDefinition); + return Result.ok(); + } + + async update(taxDefinition: TaxDefinition): Promise> { + this.updateCalls += 1; + this.items.set(taxDefinition.id.toString(), taxDefinition); + return Result.ok(); + } + + async deleteByIdInCompany(companyId: UniqueID, id: UniqueID): Promise> { + this.deleteCalls += 1; + const found = await this.getByIdInCompany(companyId, id); + if (found.isFailure) return Result.fail(found.error); + this.deleted.add(id.toString()); + return Result.ok(true); + } + + async existsByIdInCompany(companyId: UniqueID, id: UniqueID): Promise> { + const item = this.items.get(id.toString()); + return Result.ok( + item !== undefined && + !this.deleted.has(id.toString()) && + item.companyId.toString() === companyId.toString() + ); + } + + async getByIdInCompany(companyId: UniqueID, id: UniqueID): Promise> { + const item = this.items.get(id.toString()); + if ( + item === undefined || + this.deleted.has(id.toString()) || + item.companyId.toString() !== companyId.toString() + ) { + return Result.fail(new EntityNotFoundError("TaxDefinition", "id", id.toString())); + } + return Result.ok(item); + } + + async getByCodeInCompany( + companyId: UniqueID, + code: TaxDefinitionCode + ): Promise> { + const item = [...this.items.values()].find( + (candidate) => + !this.deleted.has(candidate.id.toString()) && + candidate.companyId.toString() === companyId.toString() && + candidate.code.toPrimitive() === code.toPrimitive() + ); + return item === undefined + ? Result.fail(new EntityNotFoundError("TaxDefinition", "code", code.toPrimitive())) + : Result.ok(item); + } + + async findActiveByCodeInCompany(): Promise, Error>> { + return Result.ok(Maybe.none()); + } + + async findActiveByCodesInCompany(): Promise, Error>> { + return Result.ok(new Collection([])); + } + + async findByCodesInCompany(): Promise, Error>> { + return Result.ok(new Collection([])); + } + + async findByCriteriaInCompany( + _companyId: UniqueID, + _criteria: Criteria + ): Promise, Error>> { + return Result.ok(new Collection([])); + } +} 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 index c4b81833..378f5bd8 100644 --- 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 @@ -1,15 +1,15 @@ import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common"; import { DomainError, - TextValue, + CountryCode, CountryRegionCode, TextValue, UtcDate, UniqueID, ValidationErrorCollection, type ValidationErrorDetail, - extractOrPushError, + extractOrPushError, maybeFromNullableResult, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { ITaxDefinitionCreateProps } from "../../../domain"; +import { TaxCalculationBehavior, TaxDefinitionCode, TaxDefinitionName, TaxFamily, TaxRate, TaxScope, type ITaxDefinitionCreateProps } from "../../../domain"; export interface ICreateTaxDefinitionInputMapper { map( @@ -30,15 +30,43 @@ export class CreateTaxDefinitionInputMapper implements ICreateTaxDefinitionInput const code = extractOrPushError(TaxDefinitionCode.create(dto.code), "code", errors); - const name = extractOrPushError(TextValue.create(dto.name), "name", errors); + const name = extractOrPushError(TaxDefinitionName.create(dto.name), "name", errors); const description = extractOrPushError( - TextValue.create(dto.description), + maybeFromNullableResult(dto.description, (value) => TextValue.create(value)), "description", errors ); const isActive = extractOrPushError(Result.ok(dto.is_active), "is_active", errors); + const rate = extractOrPushError(TaxRate.create({ value: Number(dto.rate.value), scale: Number(dto.rate.scale) }), "rate", errors); + const taxFamily = extractOrPushError(TaxFamily.create(dto.tax_family), "tax_family", errors); + const calculationBehavior = extractOrPushError(TaxCalculationBehavior.create(dto.calculation_behavior), "calculation_behavior", errors); + const jurisdictionCountryCode = extractOrPushError(CountryCode.create(dto.jurisdiction_country_code), "jurisdiction_country_code", errors); + const jurisdictionRegionCode = extractOrPushError(maybeFromNullableResult(dto.jurisdiction_region_code, (value) => CountryRegionCode.create(value)), "jurisdiction_region_code", errors); + const taxScope = extractOrPushError(TaxScope.create(dto.tax_scope), "tax_scope", errors); + const invoiceNote = extractOrPushError(maybeFromNullableResult(dto.invoice_note, (value) => TextValue.create(value)), "invoice_note", errors); + const allowedSurchargeCodes = extractOrPushError( + maybeFromNullableResult(dto.allowed_surcharge_codes, (values) => { + const codes: TaxDefinitionCode[] = []; + + for (const value of values) { + const codeResult = TaxDefinitionCode.create(value); + + if (codeResult.isFailure) { + return Result.fail(codeResult.error); + } + + codes.push(codeResult.data); + } + + return Result.ok(codes); + }), + "allowed_surcharge_codes", + errors + ); + const validFrom = extractOrPushError(maybeFromNullableResult(dto.valid_from, (value) => UtcDate.createFromISO(value)), "valid_from", errors); + const validTo = extractOrPushError(maybeFromNullableResult(dto.valid_to, (value) => UtcDate.createFromISO(value)), "valid_to", errors); this.throwIfValidationErrors(errors); @@ -47,18 +75,10 @@ export class CreateTaxDefinitionInputMapper implements ICreateTaxDefinitionInput 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, + rate: rate!, taxFamily: taxFamily!, calculationBehavior: calculationBehavior!, jurisdictionCountryCode: jurisdictionCountryCode!, jurisdictionRegionCode: jurisdictionRegionCode!, taxScope: taxScope!, invoiceNote: invoiceNote!, allowedSurchargeCodes: allowedSurchargeCodes!, isSystem: dto.is_system ?? false, isActive: isActive ?? true, - validFrom: dto.valid_from as any, - validTo: dto.valid_to as any, + validFrom: validFrom!, validTo: validTo!, }; return Result.ok({ id: taxDefinitionId!, props }); diff --git a/modules/catalogs/src/api/application/tax-definitions/mappers/tax-definition-input-mappers.test.ts b/modules/catalogs/src/api/application/tax-definitions/mappers/tax-definition-input-mappers.test.ts new file mode 100644 index 00000000..508cb869 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/mappers/tax-definition-input-mappers.test.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { UniqueID } from "@repo/rdx-ddd"; +import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common"; +import { CreateTaxDefinitionInputMapper } from "./create-tax-definition-input.mapper"; +import { UpdateTaxDefinitionByIdInputMapper } from "./update-tax-definition-by-id-input.mapper"; + +const dto = { + id: UniqueID.generateNewID().toString(), code: "iva_21", name: "IVA 21", description: "General", + rate: { value: "2100", scale: "2" }, tax_family: "iva", calculation_behavior: "additive", jurisdiction_country_code: "ES", jurisdiction_region_code: "ES-M", tax_scope: "domestic", invoice_note: "Nota", allowed_surcharge_codes: ["rec_5_2", "rec_1_4"], is_active: true, valid_from: "2026-01-01", valid_to: "2026-12-31", +} satisfies CreateTaxDefinitionRequestDTO; + +test("create mapper materializa VOs y conserva la escala", () => { + const result = new CreateTaxDefinitionInputMapper().map(dto, { companyId: UniqueID.generateNewID() }); + assert.equal(result.isSuccess, true); + const props = result.data.props; + assert.deepEqual(props.rate.toPrimitive(), { value: 2100, scale: 2 }); + assert.equal(props.taxFamily.toPrimitive(), "iva"); + assert.deepEqual(props.allowedSurchargeCodes.unwrap().map((code) => code.toPrimitive()), ["rec_5_2", "rec_1_4"]); +}); + +test("update mapper conserva undefined y representa null como limpieza", () => { + const mapper = new UpdateTaxDefinitionByIdInputMapper(); + const partial = mapper.map({ name: "Nuevo" }); + assert.equal(partial.isSuccess, true); + assert.equal(partial.data.description, undefined); + assert.equal(partial.data.rate, undefined); + const cleared = mapper.map({ description: null, invoice_note: null, allowed_surcharge_codes: null, valid_from: null, valid_to: null }); + assert.equal(cleared.isSuccess, true); + assert.equal(cleared.data.description?.isNone(), true); + assert.equal(cleared.data.allowedSurchargeCodes?.isNone(), true); +}); 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 index 13c1292a..2a92b250 100644 --- 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 @@ -1,14 +1,14 @@ import type { UpdateTaxDefinitionByIdRequestDTO } from "@erp/catalogs/common"; import { DomainError, - TextValue, + TextValue, UtcDate, ValidationErrorCollection, type ValidationErrorDetail, extractOrPushError, } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result, toPatchField } from "@repo/rdx-utils"; -import type { TaxDefinitionPatchProps } from "../../../domain"; +import { TaxDefinitionCode, TaxDefinitionName, TaxRate, type TaxDefinitionPatchProps } from "../../../domain"; export interface IUpdateTaxDefinitionByIdInputMapper { map(dto: UpdateTaxDefinitionByIdRequestDTO): Result; @@ -19,26 +19,56 @@ export class UpdateTaxDefinitionByIdInputMapper implements IUpdateTaxDefinitionB const errors: ValidationErrorDetail[] = []; try { - const name = extractOrPushError(TextValue.create(dto.name), "name", errors); - const description = extractOrPushError( - TextValue.create(dto.description), - "description", - errors - ); + const props: TaxDefinitionPatchProps = {}; + toPatchField(dto.name).ifSet((value) => { props.name = extractOrPushError(TaxDefinitionName.create(value), "name", errors); }); + if (dto.description === null) { + props.description = Maybe.none(); + } else if (dto.description !== undefined) { + const descriptionResult = TextValue.create(dto.description); + if (descriptionResult.isFailure) return Result.fail(descriptionResult.error); + props.description = Maybe.some(descriptionResult.data); + } + toPatchField(dto.rate).ifSet((value) => { props.rate = extractOrPushError(TaxRate.create({ value: Number(value.value), scale: Number(value.scale) }), "rate", errors); }); + if (dto.invoice_note === null) { + props.invoiceNote = Maybe.none(); + } else if (dto.invoice_note !== undefined) { + const invoiceNoteResult = TextValue.create(dto.invoice_note); + if (invoiceNoteResult.isFailure) return Result.fail(invoiceNoteResult.error); + props.invoiceNote = Maybe.some(invoiceNoteResult.data); + } + if (dto.allowed_surcharge_codes === null) { + props.allowedSurchargeCodes = Maybe.none(); + } else if (dto.allowed_surcharge_codes !== undefined) { + const codes: TaxDefinitionCode[] = []; + + for (const value of dto.allowed_surcharge_codes) { + const codeResult = TaxDefinitionCode.create(value); + + if (codeResult.isFailure) return Result.fail(codeResult.error); + + codes.push(codeResult.data); + } + + props.allowedSurchargeCodes = Maybe.some(codes); + } + if (dto.valid_from === null) { + props.validFrom = Maybe.none(); + } else if (dto.valid_from !== undefined) { + const validFromResult = UtcDate.createFromISO(dto.valid_from); + if (validFromResult.isFailure) return Result.fail(validFromResult.error); + props.validFrom = Maybe.some(validFromResult.data); + } + if (dto.valid_to === null) { + props.validTo = Maybe.none(); + } else if (dto.valid_to !== undefined) { + const validToResult = UtcDate.createFromISO(dto.valid_to); + if (validToResult.isFailure) return Result.fail(validToResult.error); + props.validTo = Maybe.some(validToResult.data); + } + toPatchField(dto.is_active).ifSet((value) => { props.isActive = value; }); 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( 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 index 992b8a7b..44ad262a 100644 --- 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 @@ -1,5 +1,5 @@ -import type { TaxDefinitionCode, TaxRate } from "@erp/catalogs/api/domain"; -import type { TextValue, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import type { TaxCalculationBehavior, TaxDefinitionCode, TaxFamily, TaxRate, TaxScope } from "@erp/catalogs/api/domain"; +import type { CountryCode, CountryRegionCode, TextValue, UniqueID, UtcDate } from "@repo/rdx-ddd"; import type { Maybe } from "@repo/rdx-utils"; export type TaxDefinitionDetail = { @@ -9,13 +9,13 @@ export type TaxDefinitionDetail = { name: TextValue; description: Maybe; rate: TaxRate; - taxFamily: string; - calculationBehavior: string; - jurisdictionCountryCode: string; - jurisdictionRegionCode: Maybe; - taxScope: string; + taxFamily: TaxFamily; + calculationBehavior: TaxCalculationBehavior; + jurisdictionCountryCode: CountryCode; + jurisdictionRegionCode: Maybe; + taxScope: TaxScope; invoiceNote: Maybe; - allowedSurchargeCodes: Maybe; + allowedSurchargeCodes: Maybe; isSystem: boolean; isActive: boolean; validFrom: Maybe; 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 index 7769f055..03d64c34 100644 --- 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 @@ -2,14 +2,15 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { ITaxDefinitionRepository } from "../repositories"; +import type { TaxDefinition } from "../../../domain"; export interface ITaxDefinitionStatusChanger { - enable(companyId: UniqueID, id: UniqueID, transaction?: unknown): Promise>; + enable(companyId: UniqueID, id: UniqueID, transaction?: unknown): Promise>; disable( companyId: UniqueID, id: UniqueID, transaction?: unknown - ): Promise>; + ): Promise>; } export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger { @@ -34,7 +35,7 @@ export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger { } } - return Result.ok(changeResult.data); + return Result.ok(taxDefinition); } public async disable(companyId: UniqueID, id: UniqueID, transaction?: unknown) { @@ -56,6 +57,6 @@ export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger { } } - return Result.ok(changeResult.data); + return Result.ok(taxDefinition); } } diff --git a/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.test.ts b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.test.ts new file mode 100644 index 00000000..f601a33e --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/snapshot-builders/full/full-tax-definition-snapshot.builder.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { TaxDefinitionDetailSchema } from "@erp/catalogs/common"; + +import { buildTaxDefinition } from "../../__tests__/tax-definition-test-support"; +import { TaxDefinitionFullSnapshotBuilder } from "./full-tax-definition-snapshot.builder"; + +const builder = new TaxDefinitionFullSnapshotBuilder(); + +test("serializa una definición completa como TaxDefinitionDetailDTO", () => { + const snapshot = builder.toOutput(buildTaxDefinition()); + const parsed = TaxDefinitionDetailSchema.parse(snapshot); + + assert.deepEqual(parsed.rate, { value: "2100", scale: "2" }); + assert.equal(parsed.tax_family, "iva"); + assert.equal(parsed.calculation_behavior, "additive"); + assert.equal(parsed.tax_scope, "domestic"); + assert.equal(parsed.jurisdiction_country_code, "ES"); + assert.equal(parsed.jurisdiction_region_code, "ES-M"); + assert.deepEqual(parsed.allowed_surcharge_codes, ["rec_5_2", "rec_1_4"]); +}); + +test("serializa allowedSurchargeCodes ninguno, uno y varios como strings", () => { + const none = TaxDefinitionDetailSchema.parse( + builder.toOutput(buildTaxDefinition({ allowedSurchargeCodes: null })) + ); + const one = TaxDefinitionDetailSchema.parse( + builder.toOutput(buildTaxDefinition({ allowedSurchargeCodes: ["rec_5_2"] })) + ); + const many = TaxDefinitionDetailSchema.parse( + builder.toOutput(buildTaxDefinition({ allowedSurchargeCodes: ["rec_5_2", "rec_1_4"] })) + ); + + assert.equal(none.allowed_surcharge_codes, null); + assert.deepEqual(one.allowed_surcharge_codes, ["rec_5_2"]); + assert.deepEqual(many.allowed_surcharge_codes, ["rec_5_2", "rec_1_4"]); +}); 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 index 4570e8e3..dc9f50a9 100644 --- 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 @@ -1,12 +1,12 @@ import type { ISnapshotBuilder } from "@erp/core/api"; - -import type { TaxDefinitionDetail } from "../../models"; +import type { TaxDefinitionDetailDTO } from "@erp/catalogs/common"; +import type { TaxDefinition } from "../../../../domain"; export interface ITaxDefinitionFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class TaxDefinitionFullSnapshotBuilder implements ITaxDefinitionFullSnapshotBuilder { - public toOutput(src: TaxDefinitionDetail): TaxDefinitionDetailDTO { + public toOutput(src: TaxDefinition): TaxDefinitionDetailDTO { return { id: src.id.toString(), company_id: src.companyId.toString(), @@ -16,21 +16,24 @@ export class TaxDefinitionFullSnapshotBuilder implements ITaxDefinitionFullSnaps (v) => v.toPrimitive(), () => null ), - rate: (src.rate as any).toPrimitive(), - tax_family: src.taxFamily, - calculation_behavior: src.calculationBehavior, - jurisdiction_country_code: src.jurisdictionCountryCode, + rate: { + value: String(src.rate.toPrimitive().value), + scale: String(src.rate.toPrimitive().scale), + }, + tax_family: src.taxFamily.toPrimitive(), + calculation_behavior: src.calculationBehavior.toPrimitive(), + jurisdiction_country_code: src.jurisdictionCountryCode.toPrimitive(), jurisdiction_region_code: src.jurisdictionRegionCode.match( - (v) => v, + (v) => v.toPrimitive(), () => null ), - tax_scope: src.taxScope, + tax_scope: src.taxScope.toPrimitive(), invoice_note: src.invoiceNote.match( (v) => v.toPrimitive(), () => null ), allowed_surcharge_codes: src.allowedSurchargeCodes.match( - (arr) => arr, + (arr) => arr.map((code) => code.toPrimitive()), () => null ), is_system: src.isSystem, @@ -43,6 +46,6 @@ export class TaxDefinitionFullSnapshotBuilder implements ITaxDefinitionFullSnaps (d) => d.toPrimitive(), () => null ), - } as unknown as TaxDefinitionDetailDTO; + }; } } diff --git a/modules/catalogs/src/api/application/tax-definitions/tax-definition-status.test.ts b/modules/catalogs/src/api/application/tax-definitions/tax-definition-status.test.ts new file mode 100644 index 00000000..2364ba93 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/tax-definition-status.test.ts @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { TaxDefinitionDetailSchema } from "@erp/catalogs/common"; + +import { TaxDefinitionCannotBeDisabledError } from "../../domain"; + +import { + InMemoryTaxDefinitionRepository, + buildTaxDefinition, +} from "./__tests__/tax-definition-test-support"; +import { TaxDefinitionStatusChanger } from "./services"; +import { TaxDefinitionFullSnapshotBuilder } from "./snapshot-builders"; +import { DisableTaxDefinitionByIdUseCase, EnableTaxDefinitionByIdUseCase } from "./use-cases"; + +const buildStatusFlows = (repository: InMemoryTaxDefinitionRepository) => { + const statusChanger = new TaxDefinitionStatusChanger(repository); + const fullSnapshotBuilder = new TaxDefinitionFullSnapshotBuilder(); + return { + enable: new EnableTaxDefinitionByIdUseCase({ statusChanger, fullSnapshotBuilder }), + disable: new DisableTaxDefinitionByIdUseCase({ statusChanger, fullSnapshotBuilder }), + }; +}; + +test("enable de una definición no-system inactiva devuelve TaxDefinitionDetailDTO activa", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isActive: false }); + await repository.create(definition); + + const result = await buildStatusFlows(repository).enable.execute( + definition.companyId, + definition.id + ); + + assert.equal(result.isSuccess, true); + const output = TaxDefinitionDetailSchema.parse(result.data); + assert.equal(output.is_active, true); + assert.equal(definition.isActive, true); + assert.equal(repository.updateCalls, 1); +}); + +test("disable de una definición no-system activa devuelve TaxDefinitionDetailDTO inactiva", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isActive: true }); + await repository.create(definition); + + const result = await buildStatusFlows(repository).disable.execute( + definition.companyId, + definition.id + ); + + assert.equal(result.isSuccess, true); + const output = TaxDefinitionDetailSchema.parse(result.data); + assert.equal(output.is_active, false); + assert.equal(definition.isActive, false); + assert.equal(repository.updateCalls, 1); +}); + +test("enable de una definición system activa es idempotente y devuelve TaxDefinitionDetailDTO", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isSystem: true, isActive: true }); + await repository.create(definition); + + const result = await buildStatusFlows(repository).enable.execute( + definition.companyId, + definition.id + ); + + assert.equal(result.isSuccess, true); + assert.equal(TaxDefinitionDetailSchema.parse(result.data).is_active, true); + assert.equal(definition.isActive, true); + assert.equal(repository.updateCalls, 0); +}); + +test("disable de una definición system se rechaza y mantiene el estado", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isSystem: true, isActive: true }); + await repository.create(definition); + + const result = await buildStatusFlows(repository).disable.execute( + definition.companyId, + definition.id + ); + + assert.equal(result.isFailure, true); + assert.ok(result.error instanceof TaxDefinitionCannotBeDisabledError); + assert.equal(definition.isActive, true); + assert.equal(repository.updateCalls, 0); +}); diff --git a/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.test.ts b/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.test.ts new file mode 100644 index 00000000..351d2480 --- /dev/null +++ b/modules/catalogs/src/api/application/tax-definitions/use-cases/delete-tax-definition-by-id.use-case.test.ts @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { TaxDefinitionCannotBeDeletedError } from "../../../domain"; +import { + InMemoryTaxDefinitionRepository, + buildTaxDefinition, +} from "../__tests__/tax-definition-test-support"; +import { TaxDefinitionDeleter, TaxDefinitionFinder } from "../services"; + +import { DeleteTaxDefinitionByIdUseCase } from "./delete-tax-definition-by-id.use-case"; + +const buildUseCase = (repository: InMemoryTaxDefinitionRepository) => + new DeleteTaxDefinitionByIdUseCase({ + finder: new TaxDefinitionFinder(repository), + deleter: new TaxDefinitionDeleter(repository), + }); + +test("delete rechaza una definición system sin ejecutar repository.delete", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isSystem: true }); + await repository.create(definition); + + const result = await buildUseCase(repository).execute(definition.companyId, definition.id); + + assert.equal(result.isFailure, true); + assert.ok(result.error instanceof TaxDefinitionCannotBeDeletedError); + assert.equal(repository.deleteCalls, 0); + assert.equal( + (await repository.getByIdInCompany(definition.companyId, definition.id)).isSuccess, + true + ); +}); + +test("delete permite una definición non-system y la elimina del repositorio", async () => { + const repository = new InMemoryTaxDefinitionRepository(); + const definition = buildTaxDefinition({ isSystem: false }); + await repository.create(definition); + + const result = await buildUseCase(repository).execute(definition.companyId, definition.id); + + assert.equal(result.isSuccess, true); + assert.equal(result.data, true); + assert.equal(repository.deleteCalls, 1); + assert.equal( + (await repository.getByIdInCompany(definition.companyId, definition.id)).isFailure, + true + ); +}); 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 index 369a0870..36d4bc46 100644 --- 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 @@ -1,16 +1,28 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { ITaxDefinitionDeleter } from "../services"; +import { TaxDefinitionCannotBeDeletedError } from "../../../domain"; +import type { ITaxDefinitionDeleter, ITaxDefinitionFinder } from "../services"; type DeleteTaxDefinitionDeps = { deleter: ITaxDefinitionDeleter; + finder: ITaxDefinitionFinder; }; export class DeleteTaxDefinitionByIdUseCase { constructor(private readonly deps: DeleteTaxDefinitionDeps) {} public async execute(companyId: UniqueID, id: UniqueID) { + const found = await this.deps.finder.getByIdInCompany(companyId, id); + if (found.isFailure) { + return found; + } + if (found.data.isSystem) { + return Result.fail( + new TaxDefinitionCannotBeDeletedError("System tax definitions cannot be deleted.") + ); + } + const result = await this.deps.deleter.deleteByIdInCompany(companyId, id); if (result.isFailure) { return result; 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 index 7f8014b8..19f79b4f 100644 --- 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 @@ -2,9 +2,11 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { ITaxDefinitionStatusChanger } from "../services"; +import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders"; type DisableTaxDefinitionDeps = { statusChanger: ITaxDefinitionStatusChanger; + fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder; }; export class DisableTaxDefinitionByIdUseCase { @@ -16,6 +18,6 @@ export class DisableTaxDefinitionByIdUseCase { return result; } - return Result.ok(result.data); + return Result.ok(this.deps.fullSnapshotBuilder.toOutput(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 index a5052009..f24325da 100644 --- 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 @@ -2,9 +2,11 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { ITaxDefinitionStatusChanger } from "../services"; +import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders"; type EnableTaxDefinitionDeps = { statusChanger: ITaxDefinitionStatusChanger; + fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder; }; export class EnableTaxDefinitionByIdUseCase { @@ -16,6 +18,6 @@ export class EnableTaxDefinitionByIdUseCase { return result; } - return Result.ok(result.data); + return Result.ok(this.deps.fullSnapshotBuilder.toOutput(result.data)); } } diff --git a/modules/catalogs/src/api/domain/tax-definitions/errors.ts b/modules/catalogs/src/api/domain/tax-definitions/errors.ts index 731ad620..cb9fcb10 100644 --- a/modules/catalogs/src/api/domain/tax-definitions/errors.ts +++ b/modules/catalogs/src/api/domain/tax-definitions/errors.ts @@ -105,6 +105,8 @@ export const isTaxDefinitionCannotBeEnabledError = ( export class TaxDefinitionCannotBeDisabledError extends DomainError { public readonly code = "TAX_DEFINITION_CANNOT_BE_DISABLED" as const; } +export class TaxDefinitionCannotBeUpdatedError extends DomainError { public readonly code = "TAX_DEFINITION_CANNOT_BE_UPDATED" as const; } +export class TaxDefinitionCannotBeDeletedError extends DomainError { public readonly code = "TAX_DEFINITION_CANNOT_BE_DELETED" as const; } export const isTaxDefinitionCannotBeDisabledError = ( e: unknown diff --git a/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.test.ts b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.test.ts new file mode 100644 index 00000000..a31035d7 --- /dev/null +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.test.ts @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { CountryCode, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { Maybe } from "@repo/rdx-utils"; +import { TaxCalculationBehavior, TaxDefinition, TaxDefinitionCode, TaxDefinitionName, TaxFamily, TaxRate, TaxScope } from "."; + +const create = (params?: { from?: string; to?: string; system?: boolean }) => TaxDefinition.create({ + companyId: UniqueID.generateNewID(), code: TaxDefinitionCode.create("iva_21").data, + name: TaxDefinitionName.create("IVA 21").data, description: Maybe.none(), + rate: TaxRate.create({ value: 2100, scale: 2 }).data, taxFamily: TaxFamily.create("iva").data, + calculationBehavior: TaxCalculationBehavior.create("additive").data, + jurisdictionCountryCode: CountryCode.create("ES").data, jurisdictionRegionCode: Maybe.none(), taxScope: TaxScope.create("domestic").data, + invoiceNote: Maybe.none(), allowedSurchargeCodes: Maybe.none(), isSystem: params?.system ?? false, isActive: true, + validFrom: params?.from ? Maybe.some(UtcDate.createFromISO(params.from).data) : Maybe.none(), validTo: params?.to ? Maybe.some(UtcDate.createFromISO(params.to).data) : Maybe.none(), +}).data; + +test("acepta vigencias iguales y rechaza rangos invertidos sin mutar", () => { + assert.ok(create({ from: "2026-01-01", to: "2026-01-01" })); + const item = create({ from: "2026-01-01", to: "2026-12-31" }); + const result = item.update({ validFrom: Maybe.some(UtcDate.createFromISO("2027-01-01").data) }); + assert.equal(result.isFailure, true); + assert.equal(item.validFrom.unwrap().toPrimitive(), "2026-01-01"); +}); + +test("protege update y disable de definiciones system, pero enable es idempotente", () => { + const item = create({ system: true }); + assert.equal(item.update({ name: TaxDefinitionName.create("Otro").data }).isFailure, true); + assert.equal(item.disable().isFailure, true); + assert.equal(item.enable().isSuccess, true); +}); + +test("permite actualizar una definición non-system", () => { + const item = create(); + const updatedName = TaxDefinitionName.create("IVA reducido"); + assert.equal(updatedName.isSuccess, true); + + const result = item.update({ name: updatedName.data }); + + assert.equal(result.isSuccess, true); + assert.equal(result.data, true); + assert.equal(item.name.toPrimitive(), "IVA reducido"); +}); 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 index fda4c524..24405a80 100644 --- a/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts +++ b/modules/catalogs/src/api/domain/tax-definitions/tax-definition.aggregate.ts @@ -12,6 +12,8 @@ import type { TaxCalculationBehavior } from "./calculation-behavior"; import { InvalidTaxDefinitionAllowedSurchargeCodesError, InvalidTaxDefinitionValidityPeriodError, + TaxDefinitionCannotBeDisabledError, + TaxDefinitionCannotBeUpdatedError, } from "./errors"; import type { TaxDefinitionCode } from "./tax-definition-code"; import type { TaxDefinitionName } from "./tax-definition-name"; @@ -226,11 +228,27 @@ export class TaxDefinition extends AggregateRoot { } public update(patchProps: TaxDefinitionPatchProps): Result { + if (this.isSystem) { + return Result.fail( + new TaxDefinitionCannotBeUpdatedError("System tax definitions cannot be updated.") + ); + } const validationResult = TaxDefinition.validatePatchProps(patchProps); if (validationResult.isFailure) { return Result.fail(validationResult.error); } + const nextValidFrom = patchProps.validFrom ?? this.validFrom; + const nextValidTo = patchProps.validTo ?? this.validTo; + if ( + nextValidFrom.isSome() && + nextValidTo.isSome() && + nextValidFrom.unwrap().toPrimitive() > nextValidTo.unwrap().toPrimitive() + ) { + return Result.fail( + new InvalidTaxDefinitionValidityPeriodError("validFrom must be <= validTo") + ); + } let hasChanges = false; // Only allow updating mutable fields @@ -290,6 +308,12 @@ export class TaxDefinition extends AggregateRoot { } public disable(): Result { + if (this.isSystem) { + return Result.fail( + new TaxDefinitionCannotBeDisabledError("System tax definitions cannot be disabled.") + ); + } + if (!this.isActive) { return Result.ok(false); } diff --git a/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts index 524e2e56..45b61b8d 100644 --- a/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts +++ b/modules/catalogs/src/api/infrastructure/tax-definitions/di/tax-definitions.di.ts @@ -95,16 +95,19 @@ export const buildTaxDefinitionsDependencies = ( deleteTaxDefinitionById: () => new DeleteTaxDefinitionByIdUseCase({ deleter, + finder, }), disableTaxDefinitionById: () => new DisableTaxDefinitionByIdUseCase({ statusChanger, + fullSnapshotBuilder, }), enableTaxDefinitionById: () => new EnableTaxDefinitionByIdUseCase({ statusChanger, + fullSnapshotBuilder, }), }, }; diff --git a/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.test.ts b/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.test.ts new file mode 100644 index 00000000..7f29c040 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { TaxDefinitionDetailDTO } from "../../../../common"; +import { GetTaxDefinitionByIdAdapter } from "./get-tax-definition-by-id.adapter"; + +test("adapta el detalle completo a camelCase y conserva nullables", () => { + const dto: TaxDefinitionDetailDTO = { + id: "11111111-1111-4111-8111-111111111111", + company_id: "22222222-2222-4222-8222-222222222222", + code: "iva_21", name: "IVA 21", description: null, + rate: { value: "2100", scale: "2" }, tax_family: "iva", calculation_behavior: "additive", + jurisdiction_country_code: "ES", jurisdiction_region_code: null, tax_scope: "domestic", + invoice_note: null, allowed_surcharge_codes: ["rec_5_2", "rec_1_4"], + is_system: true, is_active: false, valid_from: "2026-01-01", valid_to: null, + }; + + assert.deepEqual(GetTaxDefinitionByIdAdapter.fromDto(dto), { + id: dto.id, companyId: dto.company_id, code: "iva_21", name: "IVA 21", description: null, + rate: 21, taxFamily: "iva", calculationBehavior: "additive", + jurisdictionCountryCode: "ES", jurisdictionRegionCode: null, taxScope: "domestic", + invoiceNote: null, allowedSurchargeCodes: ["rec_5_2", "rec_1_4"], + isSystem: true, isActive: false, validFrom: "2026-01-01", validTo: null, + }); +}); diff --git a/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.ts b/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.ts new file mode 100644 index 00000000..c8a0b3bd --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/adapters/get-tax-definition-by-id.adapter.ts @@ -0,0 +1,36 @@ +import { Percentage } from "@repo/rdx-ddd"; + +import type { TaxDefinitionDetailDTO } from "../../../../common"; +import type { TaxDefinition } from "../entities"; + +const percentageDtoToNumber = (dto: TaxDefinitionDetailDTO["rate"]): number => { + const result = Percentage.fromObjectString(dto); + if (result.isFailure) { + throw result.error; + } + return result.data.toNumber(); +}; + +export const GetTaxDefinitionByIdAdapter = { + fromDto(dto: TaxDefinitionDetailDTO): TaxDefinition { + return { + id: dto.id, + companyId: dto.company_id, + code: dto.code, + name: dto.name, + description: dto.description, + rate: percentageDtoToNumber(dto.rate), + taxFamily: dto.tax_family, + calculationBehavior: dto.calculation_behavior, + jurisdictionCountryCode: dto.jurisdiction_country_code, + jurisdictionRegionCode: dto.jurisdiction_region_code, + taxScope: dto.tax_scope, + invoiceNote: dto.invoice_note, + allowedSurchargeCodes: dto.allowed_surcharge_codes, + isSystem: dto.is_system, + isActive: dto.is_active, + validFrom: dto.valid_from, + validTo: dto.valid_to, + }; + }, +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/adapters/index.ts b/modules/catalogs/src/web/tax-definitions/shared/adapters/index.ts new file mode 100644 index 00000000..78e1e230 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/adapters/index.ts @@ -0,0 +1,2 @@ +export * from "./get-tax-definition-by-id.adapter"; +export * from "./list-tax-definitions.adapter"; diff --git a/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.test.ts b/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.test.ts new file mode 100644 index 00000000..3458bfb2 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { ListTaxDefinitionsResponseDTO } from "../../../../common"; +import { ListTaxDefinitionsAdapter } from "./list-tax-definitions.adapter"; + +test("adapta paginación y filas ligeras preservando flags", () => { + const dto: ListTaxDefinitionsResponseDTO = { + page: 1, per_page: 25, total_pages: 3, total_items: 52, + metadata: { entity: "tax_definitions", criteria: { q: "iva", filters: [], orderBy: "code", orderType: "ASC", pageSize: 25, pageNumber: 1 } }, + items: [ + { id: "11111111-1111-4111-8111-111111111111", company_id: "22222222-2222-4222-8222-222222222222", code: "iva_21", name: "IVA 21", is_active: true, is_system: false }, + { id: "33333333-3333-4333-8333-333333333333", company_id: "22222222-2222-4222-8222-222222222222", code: "iva_10", name: "IVA 10", is_active: false, is_system: true }, + ], + }; + + assert.deepEqual(ListTaxDefinitionsAdapter.fromDto(dto), { + page: 1, perPage: 25, totalPages: 3, totalItems: 52, + items: [ + { id: dto.items[0].id, companyId: dto.items[0].company_id, code: "iva_21", name: "IVA 21", isActive: true, isSystem: false }, + { id: dto.items[1].id, companyId: dto.items[1].company_id, code: "iva_10", name: "IVA 10", isActive: false, isSystem: true }, + ], + }); +}); diff --git a/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.ts b/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.ts new file mode 100644 index 00000000..6ea00e39 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/adapters/list-tax-definitions.adapter.ts @@ -0,0 +1,17 @@ +import type { ListTaxDefinitionsResponseDTO } from "../../../../common"; +import type { ListTaxDefinitionsResult } from "../api"; +import type { TaxDefinitionList, TaxDefinitionListRow } from "../entities"; + +type TaxDefinitionListItemDTO = ListTaxDefinitionsResponseDTO["items"][number]; + +const TaxDefinitionListRowAdapter = { + fromDto(dto: TaxDefinitionListItemDTO): TaxDefinitionListRow { + return { id: dto.id, companyId: dto.company_id, code: dto.code, name: dto.name, isSystem: dto.is_system, isActive: dto.is_active }; + }, +}; + +export const ListTaxDefinitionsAdapter = { + fromDto(dto: ListTaxDefinitionsResult): TaxDefinitionList { + return { page: dto.page, perPage: dto.per_page, totalPages: dto.total_pages, totalItems: dto.total_items, items: dto.items.map(TaxDefinitionListRowAdapter.fromDto) }; + }, +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/create-tax-definition.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/create-tax-definition.api.ts new file mode 100644 index 00000000..4741c8bf --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/create-tax-definition.api.ts @@ -0,0 +1,8 @@ +import type { IDataSource } from "@erp/core/client"; +import type { CreateTaxDefinitionRequestDTO, CreateTaxDefinitionResponseDTO } from "../../../../common"; + +export interface CreateTaxDefinitionParams { id: string; data: CreateTaxDefinitionRequestDTO; } +export type CreateTaxDefinitionResult = CreateTaxDefinitionResponseDTO; + +export const createTaxDefinition = (dataSource: IDataSource, params: CreateTaxDefinitionParams) => + dataSource.createOne("catalogs/tax-definitions", params.data); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/delete-tax-definition-by-id.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/delete-tax-definition-by-id.api.ts new file mode 100644 index 00000000..b6e3aa46 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/delete-tax-definition-by-id.api.ts @@ -0,0 +1,7 @@ +import type { IDataSource } from "@erp/core/client"; + +export interface DeleteTaxDefinitionByIdParams { taxDefinitionId: string; } +export type DeleteTaxDefinitionByIdResult = void; + +export const deleteTaxDefinitionById = (dataSource: IDataSource, params: DeleteTaxDefinitionByIdParams) => + dataSource.deleteOne("catalogs/tax-definitions", params.taxDefinitionId); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/disable-tax-definition-by-id.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/disable-tax-definition-by-id.api.ts new file mode 100644 index 00000000..dcf4d55a --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/disable-tax-definition-by-id.api.ts @@ -0,0 +1,8 @@ +import type { IDataSource } from "@erp/core/client"; +import type { DisableTaxDefinitionByIdResponseDTO } from "../../../../common"; + +export interface DisableTaxDefinitionByIdParams { taxDefinitionId: string; } +export type DisableTaxDefinitionByIdResult = DisableTaxDefinitionByIdResponseDTO; + +export const disableTaxDefinitionById = (dataSource: IDataSource, params: DisableTaxDefinitionByIdParams) => + dataSource.custom({ method: "patch", path: `catalogs/tax-definitions/${params.taxDefinitionId}/disable`, data: undefined }); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/enable-tax-definition-by-id.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/enable-tax-definition-by-id.api.ts new file mode 100644 index 00000000..08401b75 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/enable-tax-definition-by-id.api.ts @@ -0,0 +1,8 @@ +import type { IDataSource } from "@erp/core/client"; +import type { EnableTaxDefinitionByIdResponseDTO } from "../../../../common"; + +export interface EnableTaxDefinitionByIdParams { taxDefinitionId: string; } +export type EnableTaxDefinitionByIdResult = EnableTaxDefinitionByIdResponseDTO; + +export const enableTaxDefinitionById = (dataSource: IDataSource, params: EnableTaxDefinitionByIdParams) => + dataSource.custom({ method: "patch", path: `catalogs/tax-definitions/${params.taxDefinitionId}/enable`, data: undefined }); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/get-tax-definition-by-id.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/get-tax-definition-by-id.api.ts new file mode 100644 index 00000000..5a9b0147 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/get-tax-definition-by-id.api.ts @@ -0,0 +1,7 @@ +import type { IDataSource } from "@erp/core/client"; +import type { GetTaxDefinitionByIdResponseDTO } from "../../../../common"; + +export interface GetTaxDefinitionByIdParams { taxDefinitionId: string; signal?: AbortSignal; } + +export const getTaxDefinitionById = (dataSource: IDataSource, params: GetTaxDefinitionByIdParams) => + dataSource.getOne("catalogs/tax-definitions", params.taxDefinitionId, { signal: params.signal }); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/index.ts b/modules/catalogs/src/web/tax-definitions/shared/api/index.ts new file mode 100644 index 00000000..18fef60c --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/index.ts @@ -0,0 +1,7 @@ +export * from "./create-tax-definition.api"; +export * from "./delete-tax-definition-by-id.api"; +export * from "./disable-tax-definition-by-id.api"; +export * from "./enable-tax-definition-by-id.api"; +export * from "./get-tax-definition-by-id.api"; +export * from "./list-tax-definitions.api"; +export * from "./update-tax-definition-by-id.api"; diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/list-tax-definitions.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/list-tax-definitions.api.ts new file mode 100644 index 00000000..217788ba --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/list-tax-definitions.api.ts @@ -0,0 +1,12 @@ +import type { CriteriaDTO } from "@erp/core"; +import type { IDataSource } from "@erp/core/client"; +import type { ListTaxDefinitionsResponseDTO } from "../../../../common"; + +export interface ListTaxDefinitionsParams { criteria?: Partial; signal?: AbortSignal; } +export type ListTaxDefinitionsResult = ListTaxDefinitionsResponseDTO; + +export const listTaxDefinitions = (dataSource: IDataSource, params: ListTaxDefinitionsParams) => + dataSource.getList("catalogs/tax-definitions", { + signal: params.signal, + ...params.criteria, + }); diff --git a/modules/catalogs/src/web/tax-definitions/shared/api/update-tax-definition-by-id.api.ts b/modules/catalogs/src/web/tax-definitions/shared/api/update-tax-definition-by-id.api.ts new file mode 100644 index 00000000..fb45edc7 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/api/update-tax-definition-by-id.api.ts @@ -0,0 +1,8 @@ +import type { IDataSource } from "@erp/core/client"; +import type { UpdateTaxDefinitionByIdRequestDTO, UpdateTaxDefinitionByIdResponseDTO } from "../../../../common"; + +export interface UpdateTaxDefinitionByIdParams { taxDefinitionId: string; data: UpdateTaxDefinitionByIdRequestDTO; } +export type UpdateTaxDefinitionByIdResult = UpdateTaxDefinitionByIdResponseDTO; + +export const updateTaxDefinitionById = (dataSource: IDataSource, params: UpdateTaxDefinitionByIdParams) => + dataSource.updateOne("catalogs/tax-definitions", params.taxDefinitionId, params.data); diff --git a/modules/catalogs/src/web/tax-definitions/shared/entities/index.ts b/modules/catalogs/src/web/tax-definitions/shared/entities/index.ts new file mode 100644 index 00000000..67a75858 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./tax-definition.entity"; +export * from "./tax-definition-list.entity"; +export * from "./tax-definition-list-row.entity"; diff --git a/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list-row.entity.ts b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list-row.entity.ts new file mode 100644 index 00000000..46930a56 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list-row.entity.ts @@ -0,0 +1,8 @@ +export interface TaxDefinitionListRow { + id: string; + companyId: string; + code: string; + name: string; + isSystem: boolean; + isActive: boolean; +} diff --git a/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list.entity.ts b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list.entity.ts new file mode 100644 index 00000000..ca2d6eb7 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition-list.entity.ts @@ -0,0 +1,9 @@ +import type { TaxDefinitionListRow } from "./tax-definition-list-row.entity"; + +export interface TaxDefinitionList { + items: TaxDefinitionListRow[]; + page: number; + perPage: number; + totalPages: number; + totalItems: number; +} diff --git a/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition.entity.ts b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition.entity.ts new file mode 100644 index 00000000..fb70a113 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/entities/tax-definition.entity.ts @@ -0,0 +1,25 @@ +export type TaxDefinitionFamily = + | "iva" | "igic" | "ipsi" | "equivalence_surcharge" | "withholding" | "vat" + | "gst" | "sales_tax" | "reverse_charge" | "exempt" | "not_subject" | "custom"; +export type TaxCalculationBehavior = "additive" | "subtractive" | "neutral"; +export type TaxScope = "domestic" | "intra_eu" | "export" | "import" | "international"; + +export interface TaxDefinition { + id: string; + companyId: string; + code: string; + name: string; + description: string | null; + rate: number; + taxFamily: TaxDefinitionFamily; + calculationBehavior: TaxCalculationBehavior; + jurisdictionCountryCode: string; + jurisdictionRegionCode: string | null; + taxScope: TaxScope; + invoiceNote: string | null; + allowedSurchargeCodes: string[] | null; + isSystem: boolean; + isActive: boolean; + validFrom: string | null; + validTo: string | null; +} diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/index.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/index.ts new file mode 100644 index 00000000..f61af842 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/index.ts @@ -0,0 +1,8 @@ +export * from "./keys"; +export * from "./use-tax-definition-create-mutation"; +export * from "./use-tax-definition-delete-mutation"; +export * from "./use-tax-definition-detail-query"; +export * from "./use-tax-definition-disable-mutation"; +export * from "./use-tax-definition-enable-mutation"; +export * from "./use-tax-definition-list-query"; +export * from "./use-tax-definition-update-mutation"; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/keys.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/keys.ts new file mode 100644 index 00000000..f93cecfb --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/keys.ts @@ -0,0 +1,33 @@ +import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; +import type { QueryKey } from "@tanstack/react-query"; +import type { ListTaxDefinitionsRequestDTO } from "../../../../common"; + +export const LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX = ["tax-definitions"] as const; +export const TAX_DEFINITIONS_DETAIL_QUERY_KEY_PREFIX = ["tax-definitions:detail"] as const; + +export const LIST_TAX_DEFINITIONS_QUERY_KEY = (criteria?: Partial): QueryKey => [ + ...LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, + { + pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX, + pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE, + q: criteria?.q ?? "", + filters: criteria?.filters ?? [], + orderBy: criteria?.orderBy ?? "", + order: criteria?.order ?? "", + }, +] as const; +export const TAX_DEFINITION_QUERY_KEY = (taxDefinitionId?: string): QueryKey => [ + ...TAX_DEFINITIONS_DETAIL_QUERY_KEY_PREFIX, + { taxDefinitionId }, +]; +export const CREATE_TAX_DEFINITION_MUTATION_KEY = ["tax-definitions:create"] as const; +export const UPDATE_TAX_DEFINITION_MUTATION_KEY = ["tax-definitions:update"] as const; +export const DELETE_TAX_DEFINITION_MUTATION_KEY = ["tax-definitions:delete"] as const; +export const ENABLE_TAX_DEFINITION_MUTATION_KEY = ["tax-definitions:enable"] as const; +export const DISABLE_TAX_DEFINITION_MUTATION_KEY = ["tax-definitions:disable"] as const; + +export const taxDefinitionKeys = { + all: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, + list: (criteria?: Partial) => LIST_TAX_DEFINITIONS_QUERY_KEY(criteria), + detail: (taxDefinitionId?: string) => TAX_DEFINITION_QUERY_KEY(taxDefinitionId), +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/to-validation-errors.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/to-validation-errors.ts new file mode 100644 index 00000000..7d3289e0 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/to-validation-errors.ts @@ -0,0 +1,5 @@ +import type { z } from "zod/v4"; +import type { ValidationErrorDetail } from "@repo/rdx-ddd"; + +export const toValidationErrors = (error: z.ZodError): ValidationErrorDetail[] => + error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message })); diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-create-mutation.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-create-mutation.ts new file mode 100644 index 00000000..0ce90260 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-create-mutation.ts @@ -0,0 +1,27 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { CreateTaxDefinitionRequestSchema } from "../../../../common"; +import { GetTaxDefinitionByIdAdapter } from "../adapters"; +import { createTaxDefinition, type CreateTaxDefinitionParams } from "../api"; +import type { TaxDefinition } from "../entities"; +import { CREATE_TAX_DEFINITION_MUTATION_KEY, LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, TAX_DEFINITION_QUERY_KEY } from "./keys"; +import { toValidationErrors } from "./to-validation-errors"; + +export const useTaxDefinitionCreateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + return useMutation({ + mutationKey: CREATE_TAX_DEFINITION_MUTATION_KEY, + mutationFn: async (params) => { + if (!params.id) throw new Error("taxDefinitionId is required"); + const validation = CreateTaxDefinitionRequestSchema.safeParse(params.data); + if (!validation.success) throw new ValidationErrorCollection("Validation failed", toValidationErrors(validation.error)); + return GetTaxDefinitionByIdAdapter.fromDto(await createTaxDefinition(dataSource, params)); + }, + onSuccess: async (taxDefinition) => { + queryClient.setQueryData(TAX_DEFINITION_QUERY_KEY(taxDefinition.id), taxDefinition); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-delete-mutation.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-delete-mutation.ts new file mode 100644 index 00000000..df3cd74b --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-delete-mutation.ts @@ -0,0 +1,17 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteTaxDefinitionById, type DeleteTaxDefinitionByIdParams } from "../api"; +import { DELETE_TAX_DEFINITION_MUTATION_KEY, LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, TAX_DEFINITION_QUERY_KEY } from "./keys"; + +export const useTaxDefinitionDeleteMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + return useMutation({ + mutationKey: DELETE_TAX_DEFINITION_MUTATION_KEY, + mutationFn: async (params) => { await deleteTaxDefinitionById(dataSource, params); }, + onSuccess: async (_result, params) => { + queryClient.removeQueries({ queryKey: TAX_DEFINITION_QUERY_KEY(params.taxDefinitionId) }); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-detail-query.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-detail-query.ts new file mode 100644 index 00000000..f8f6b8ff --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-detail-query.ts @@ -0,0 +1,16 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; +import { GetTaxDefinitionByIdAdapter } from "../adapters"; +import { getTaxDefinitionById } from "../api"; +import type { TaxDefinition } from "../entities"; +import { TAX_DEFINITION_QUERY_KEY } from "./keys"; + +export const useTaxDefinitionDetailQuery = (taxDefinitionId?: string, options?: { enabled?: boolean }): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = (options?.enabled ?? true) && Boolean(taxDefinitionId); + return useQuery({ + queryKey: TAX_DEFINITION_QUERY_KEY(taxDefinitionId), + enabled, + queryFn: async ({ signal }) => GetTaxDefinitionByIdAdapter.fromDto(await getTaxDefinitionById(dataSource, { taxDefinitionId: taxDefinitionId!, signal })), + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-disable-mutation.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-disable-mutation.ts new file mode 100644 index 00000000..570b299b --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-disable-mutation.ts @@ -0,0 +1,19 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { GetTaxDefinitionByIdAdapter } from "../adapters"; +import { disableTaxDefinitionById, type DisableTaxDefinitionByIdParams } from "../api"; +import type { TaxDefinition } from "../entities"; +import { DISABLE_TAX_DEFINITION_MUTATION_KEY, LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, TAX_DEFINITION_QUERY_KEY } from "./keys"; + +export const useTaxDefinitionDisableMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + return useMutation({ + mutationKey: DISABLE_TAX_DEFINITION_MUTATION_KEY, + mutationFn: async (params) => GetTaxDefinitionByIdAdapter.fromDto(await disableTaxDefinitionById(dataSource, params)), + onSuccess: async (taxDefinition) => { + queryClient.setQueryData(TAX_DEFINITION_QUERY_KEY(taxDefinition.id), taxDefinition); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-enable-mutation.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-enable-mutation.ts new file mode 100644 index 00000000..c5448b87 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-enable-mutation.ts @@ -0,0 +1,19 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { GetTaxDefinitionByIdAdapter } from "../adapters"; +import { enableTaxDefinitionById, type EnableTaxDefinitionByIdParams } from "../api"; +import type { TaxDefinition } from "../entities"; +import { ENABLE_TAX_DEFINITION_MUTATION_KEY, LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, TAX_DEFINITION_QUERY_KEY } from "./keys"; + +export const useTaxDefinitionEnableMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + return useMutation({ + mutationKey: ENABLE_TAX_DEFINITION_MUTATION_KEY, + mutationFn: async (params) => GetTaxDefinitionByIdAdapter.fromDto(await enableTaxDefinitionById(dataSource, params)), + onSuccess: async (taxDefinition) => { + queryClient.setQueryData(TAX_DEFINITION_QUERY_KEY(taxDefinition.id), taxDefinition); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-list-query.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-list-query.ts new file mode 100644 index 00000000..3f51c629 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-list-query.ts @@ -0,0 +1,20 @@ +import type { CriteriaDTO } from "@erp/core"; +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; +import { ListTaxDefinitionsAdapter } from "../adapters"; +import { listTaxDefinitions } from "../api"; +import type { TaxDefinitionList } from "../entities"; +import { LIST_TAX_DEFINITIONS_QUERY_KEY } from "./keys"; + +export interface TaxDefinitionListQueryOptions { enabled?: boolean; criteria?: Partial; } + +export const useTaxDefinitionListQuery = (options?: TaxDefinitionListQueryOptions): UseQueryResult => { + const dataSource = useDataSource(); + const criteria = options?.criteria ?? {}; + return useQuery({ + queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY(criteria), + enabled: options?.enabled ?? true, + queryFn: async ({ signal }) => ListTaxDefinitionsAdapter.fromDto(await listTaxDefinitions(dataSource, { signal, criteria })), + placeholderData: (previousData) => previousData, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-update-mutation.ts b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-update-mutation.ts new file mode 100644 index 00000000..a8bcd202 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/hooks/use-tax-definition-update-mutation.ts @@ -0,0 +1,27 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { UpdateTaxDefinitionByIdRequestSchema } from "../../../../common"; +import { GetTaxDefinitionByIdAdapter } from "../adapters"; +import { type UpdateTaxDefinitionByIdParams, updateTaxDefinitionById } from "../api"; +import type { TaxDefinition } from "../entities"; +import { LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX, TAX_DEFINITION_QUERY_KEY, UPDATE_TAX_DEFINITION_MUTATION_KEY } from "./keys"; +import { toValidationErrors } from "./to-validation-errors"; + +export const useTaxDefinitionUpdateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + return useMutation({ + mutationKey: UPDATE_TAX_DEFINITION_MUTATION_KEY, + mutationFn: async (params) => { + if (!params.taxDefinitionId) throw new Error("taxDefinitionId is required"); + const validation = UpdateTaxDefinitionByIdRequestSchema.safeParse(params.data); + if (!validation.success) throw new ValidationErrorCollection("Validation failed", toValidationErrors(validation.error)); + return GetTaxDefinitionByIdAdapter.fromDto(await updateTaxDefinitionById(dataSource, params)); + }, + onSuccess: async (taxDefinition) => { + queryClient.setQueryData(TAX_DEFINITION_QUERY_KEY(taxDefinition.id), taxDefinition); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-definitions/shared/index.ts b/modules/catalogs/src/web/tax-definitions/shared/index.ts new file mode 100644 index 00000000..01b46929 --- /dev/null +++ b/modules/catalogs/src/web/tax-definitions/shared/index.ts @@ -0,0 +1,4 @@ +export * from "./adapters"; +export * from "./api"; +export * from "./entities"; +export * from "./hooks";