feat(tax-definitions): add comprehensive tests and implement CRUD operations for tax definitions

- Introduced unit tests for input mappers and snapshot builders to ensure correct mapping and serialization of tax definitions.
- Implemented status change use cases for enabling and disabling tax definitions with appropriate error handling.
- Added API endpoints for creating, deleting, enabling, and disabling tax definitions, along with corresponding hooks for React.
- Developed entity structures for tax definitions and their lists, ensuring proper data handling in the application.
- Enhanced validation and error handling in mutation hooks for creating and updating tax definitions.
This commit is contained in:
David Arranz 2026-09-11 13:13:58 +02:00
parent 85973c6096
commit aed24d5354
45 changed files with 962 additions and 62 deletions

View File

@ -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 = <T>(result: Result<T, Error>): 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<string, TaxDefinition>();
private readonly deleted = new Set<string>();
public updateCalls = 0;
public deleteCalls = 0;
async create(taxDefinition: TaxDefinition): Promise<Result<void, Error>> {
this.items.set(taxDefinition.id.toString(), taxDefinition);
return Result.ok();
}
async update(taxDefinition: TaxDefinition): Promise<Result<void, Error>> {
this.updateCalls += 1;
this.items.set(taxDefinition.id.toString(), taxDefinition);
return Result.ok();
}
async deleteByIdInCompany(companyId: UniqueID, id: UniqueID): Promise<Result<boolean, Error>> {
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<Result<boolean, Error>> {
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<Result<TaxDefinition, Error>> {
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<Result<TaxDefinition, Error>> {
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<Result<Maybe<TaxDefinition>, Error>> {
return Result.ok(Maybe.none());
}
async findActiveByCodesInCompany(): Promise<Result<Collection<TaxDefinition>, Error>> {
return Result.ok(new Collection<TaxDefinition>([]));
}
async findByCodesInCompany(): Promise<Result<Collection<TaxDefinition>, Error>> {
return Result.ok(new Collection<TaxDefinition>([]));
}
async findByCriteriaInCompany(
_companyId: UniqueID,
_criteria: Criteria
): Promise<Result<Collection<TaxDefinitionSummary>, Error>> {
return Result.ok(new Collection<TaxDefinitionSummary>([]));
}
}

View File

@ -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 });

View File

@ -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);
});

View File

@ -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<TaxDefinitionPatchProps, Error>;
@ -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(

View File

@ -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<TextValue>;
rate: TaxRate;
taxFamily: string;
calculationBehavior: string;
jurisdictionCountryCode: string;
jurisdictionRegionCode: Maybe<string>;
taxScope: string;
taxFamily: TaxFamily;
calculationBehavior: TaxCalculationBehavior;
jurisdictionCountryCode: CountryCode;
jurisdictionRegionCode: Maybe<CountryRegionCode>;
taxScope: TaxScope;
invoiceNote: Maybe<TextValue>;
allowedSurchargeCodes: Maybe<string[]>;
allowedSurchargeCodes: Maybe<TaxDefinitionCode[]>;
isSystem: boolean;
isActive: boolean;
validFrom: Maybe<UtcDate>;

View File

@ -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<Result<boolean, Error>>;
enable(companyId: UniqueID, id: UniqueID, transaction?: unknown): Promise<Result<TaxDefinition, Error>>;
disable(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
): Promise<Result<TaxDefinition, Error>>;
}
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);
}
}

View File

@ -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"]);
});

View File

@ -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<TaxDefinitionDetail, TaxDefinitionDetailDTO> {}
extends ISnapshotBuilder<TaxDefinition, TaxDefinitionDetailDTO> {}
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;
};
}
}

View File

@ -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);
});

View File

@ -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
);
});

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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

View File

@ -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");
});

View File

@ -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<TaxDefinitionInternalProps> {
}
public update(patchProps: TaxDefinitionPatchProps): Result<boolean, Error> {
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<TaxDefinitionInternalProps> {
}
public disable(): Result<boolean, Error> {
if (this.isSystem) {
return Result.fail(
new TaxDefinitionCannotBeDisabledError("System tax definitions cannot be disabled.")
);
}
if (!this.isActive) {
return Result.ok(false);
}

View File

@ -95,16 +95,19 @@ export const buildTaxDefinitionsDependencies = (
deleteTaxDefinitionById: () =>
new DeleteTaxDefinitionByIdUseCase({
deleter,
finder,
}),
disableTaxDefinitionById: () =>
new DisableTaxDefinitionByIdUseCase({
statusChanger,
fullSnapshotBuilder,
}),
enableTaxDefinitionById: () =>
new EnableTaxDefinitionByIdUseCase({
statusChanger,
fullSnapshotBuilder,
}),
},
};

View File

@ -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,
});
});

View File

@ -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,
};
},
};

View File

@ -0,0 +1,2 @@
export * from "./get-tax-definition-by-id.adapter";
export * from "./list-tax-definitions.adapter";

View File

@ -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 },
],
});
});

View File

@ -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) };
},
};

View File

@ -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<CreateTaxDefinitionRequestDTO, CreateTaxDefinitionResponseDTO>("catalogs/tax-definitions", params.data);

View File

@ -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);

View File

@ -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<undefined, DisableTaxDefinitionByIdResponseDTO>({ method: "patch", path: `catalogs/tax-definitions/${params.taxDefinitionId}/disable`, data: undefined });

View File

@ -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<undefined, EnableTaxDefinitionByIdResponseDTO>({ method: "patch", path: `catalogs/tax-definitions/${params.taxDefinitionId}/enable`, data: undefined });

View File

@ -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<GetTaxDefinitionByIdResponseDTO>("catalogs/tax-definitions", params.taxDefinitionId, { signal: params.signal });

View File

@ -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";

View File

@ -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<CriteriaDTO>; signal?: AbortSignal; }
export type ListTaxDefinitionsResult = ListTaxDefinitionsResponseDTO;
export const listTaxDefinitions = (dataSource: IDataSource, params: ListTaxDefinitionsParams) =>
dataSource.getList<ListTaxDefinitionsResponseDTO>("catalogs/tax-definitions", {
signal: params.signal,
...params.criteria,
});

View File

@ -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<UpdateTaxDefinitionByIdRequestDTO, UpdateTaxDefinitionByIdResponseDTO>("catalogs/tax-definitions", params.taxDefinitionId, params.data);

View File

@ -0,0 +1,3 @@
export * from "./tax-definition.entity";
export * from "./tax-definition-list.entity";
export * from "./tax-definition-list-row.entity";

View File

@ -0,0 +1,8 @@
export interface TaxDefinitionListRow {
id: string;
companyId: string;
code: string;
name: string;
isSystem: boolean;
isActive: boolean;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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<ListTaxDefinitionsRequestDTO>): 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<ListTaxDefinitionsRequestDTO>) => LIST_TAX_DEFINITIONS_QUERY_KEY(criteria),
detail: (taxDefinitionId?: string) => TAX_DEFINITION_QUERY_KEY(taxDefinitionId),
};

View File

@ -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 }));

View File

@ -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<TaxDefinition, DefaultError, CreateTaxDefinitionParams>({
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 });
},
});
};

View File

@ -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<void, DefaultError, DeleteTaxDefinitionByIdParams>({
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 });
},
});
};

View File

@ -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<TaxDefinition, DefaultError> => {
const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && Boolean(taxDefinitionId);
return useQuery<TaxDefinition, DefaultError>({
queryKey: TAX_DEFINITION_QUERY_KEY(taxDefinitionId),
enabled,
queryFn: async ({ signal }) => GetTaxDefinitionByIdAdapter.fromDto(await getTaxDefinitionById(dataSource, { taxDefinitionId: taxDefinitionId!, signal })),
});
};

View File

@ -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<TaxDefinition, DefaultError, DisableTaxDefinitionByIdParams>({
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 });
},
});
};

View File

@ -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<TaxDefinition, DefaultError, EnableTaxDefinitionByIdParams>({
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 });
},
});
};

View File

@ -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<CriteriaDTO>; }
export const useTaxDefinitionListQuery = (options?: TaxDefinitionListQueryOptions): UseQueryResult<TaxDefinitionList, DefaultError> => {
const dataSource = useDataSource();
const criteria = options?.criteria ?? {};
return useQuery<TaxDefinitionList, DefaultError>({
queryKey: LIST_TAX_DEFINITIONS_QUERY_KEY(criteria),
enabled: options?.enabled ?? true,
queryFn: async ({ signal }) => ListTaxDefinitionsAdapter.fromDto(await listTaxDefinitions(dataSource, { signal, criteria })),
placeholderData: (previousData) => previousData,
});
};

View File

@ -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<TaxDefinition, DefaultError, UpdateTaxDefinitionByIdParams>({
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 });
},
});
};

View File

@ -0,0 +1,4 @@
export * from "./adapters";
export * from "./api";
export * from "./entities";
export * from "./hooks";