This commit is contained in:
David Arranz 2026-06-05 16:49:35 +02:00
parent cea1ab2766
commit dc6382dd41
96 changed files with 2516 additions and 381 deletions

View File

@ -16,8 +16,7 @@
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts",
"./client/payment-methods": "./src/web/payment-methods/index.ts",
"./client/payment-terms": "./src/web/payment-terms/index.ts"
"./client/*": "./src/web/*/index.ts"
},
"peerDependencies": {
"react": "^19.2.5",

View File

@ -1,3 +1,5 @@
export * from "./payment-methods";
export * from "./payment-terms";
export * from "./services";
export * from "./tax-definitions";
export * from "./tax-regimes";

View File

@ -0,0 +1,20 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { IPaymentMethodPublicServices } from "../payment-methods";
import type { IPaymentTermPublicServices } from "../payment-terms";
import type { ITaxRegimePublicServices } from "../tax-regimes";
export type { IPaymentMethodPublicServices } from "../payment-methods";
export type { IPaymentTermPublicServices } from "../payment-terms";
export type { ITaxRegimePublicServices } from "../tax-regimes";
export interface ICatalogServicesContext {
transaction: unknown;
companyId: UniqueID;
}
export interface ICatalogPublicServices {
paymentMethods: IPaymentMethodPublicServices;
paymentTerms: IPaymentTermPublicServices;
taxRegimes: ITaxRegimePublicServices;
}

View File

@ -0,0 +1 @@
export * from './catalog-public-services.interface';

View File

@ -0,0 +1,6 @@
export * from "./mappers";
export * from "./models";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases";

View File

@ -0,0 +1,75 @@
import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common";
import {
DomainError,
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionCreateProps, TaxDefinitionCode } from "../../../domain";
export interface ICreateTaxDefinitionInputMapper {
map(
dto: CreateTaxDefinitionRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ITaxDefinitionCreateProps }, Error>;
}
export class CreateTaxDefinitionInputMapper implements ICreateTaxDefinitionInputMapper {
public map(
dto: CreateTaxDefinitionRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ITaxDefinitionCreateProps }, Error> {
const errors: ValidationErrorDetail[] = [];
try {
const taxDefinitionId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const code = extractOrPushError(TaxDefinitionCode.create(dto.code), "code", errors);
const name = extractOrPushError(TextValue.create(dto.name), "name", errors);
const description = extractOrPushError(
TextValue.create(dto.description),
"description",
errors
);
const isActive = extractOrPushError(Result.ok(dto.is_active), "is_active", errors);
this.throwIfValidationErrors(errors);
const props: ITaxDefinitionCreateProps = {
companyId: params.companyId,
code: code!,
name: name!,
description: description!,
rate: dto.rate as any,
taxFamily: dto.tax_family as any,
calculationBehavior: dto.calculation_behavior as any,
jurisdictionCountryCode: dto.jurisdiction_country_code as any,
jurisdictionRegionCode: dto.jurisdiction_region_code as any,
taxScope: dto.tax_scope as any,
invoiceNote: dto.invoice_note as any,
allowedSurchargeCodes: dto.allowed_surcharge_codes as any,
isSystem: dto.is_system ?? false,
isActive: isActive ?? true,
validFrom: dto.valid_from as any,
validTo: dto.valid_to as any,
};
return Result.ok({ id: taxDefinitionId!, props });
} catch (err: unknown) {
return Result.fail(new DomainError("Tax definition props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Tax definition props mapping failed", errors);
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./create-tax-definition-input.mapper";
export * from "./update-tax-definition-by-id-input.mapper";

View File

@ -0,0 +1,55 @@
import type { UpdateTaxDefinitionByIdRequestDTO } from "@erp/catalogs/common";
import {
DomainError,
TextValue,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { TaxDefinitionPatchProps } from "../../../domain";
export interface IUpdateTaxDefinitionByIdInputMapper {
map(dto: UpdateTaxDefinitionByIdRequestDTO): Result<TaxDefinitionPatchProps, Error>;
}
export class UpdateTaxDefinitionByIdInputMapper implements IUpdateTaxDefinitionByIdInputMapper {
public map(dto: UpdateTaxDefinitionByIdRequestDTO): Result<TaxDefinitionPatchProps, Error> {
const errors: ValidationErrorDetail[] = [];
try {
const name = extractOrPushError(TextValue.create(dto.name), "name", errors);
const description = extractOrPushError(
TextValue.create(dto.description),
"description",
errors
);
this.throwIfValidationErrors(errors);
const props: TaxDefinitionPatchProps = {
name: name ?? undefined,
description: description ?? undefined,
invoiceNote: dto.invoice_note as any,
rate: dto.rate as any,
allowedSurchargeCodes: dto.allowed_surcharge_codes as any,
isActive: dto.is_active,
validFrom: dto.valid_from as any,
validTo: dto.valid_to as any,
};
return Result.ok(props);
} catch (err: unknown) {
return Result.fail(
new DomainError("Tax definition update props mapping failed", { cause: err })
);
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Tax definition update props mapping failed", errors);
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./tax-definition-detail.model";
export * from "./tax-definition-summary.model";

View File

@ -0,0 +1,23 @@
import type { TaxDefinitionCode, TaxRate } from "@erp/catalogs/api/domain";
import type { TextValue, UniqueID, UtcDate } from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
export type TaxDefinitionDetail = {
id: UniqueID;
companyId: UniqueID;
code: TaxDefinitionCode;
name: TextValue;
description: Maybe<TextValue>;
rate: TaxRate;
taxFamily: string;
calculationBehavior: string;
jurisdictionCountryCode: string;
jurisdictionRegionCode: Maybe<string>;
taxScope: string;
invoiceNote: Maybe<TextValue>;
allowedSurchargeCodes: Maybe<string[]>;
isSystem: boolean;
isActive: boolean;
validFrom: Maybe<UtcDate>;
validTo: Maybe<UtcDate>;
};

View File

@ -0,0 +1,11 @@
import type { TaxDefinitionCode } from "@erp/catalogs/api/domain";
import type { TextValue, UniqueID } from "@repo/rdx-ddd";
export type TaxDefinitionSummary = {
id: UniqueID;
companyId: UniqueID;
code: TaxDefinitionCode;
name: TextValue;
isActive: boolean;
isSystem: boolean;
};

View File

@ -0,0 +1 @@
export * from './tax-definition-repository.interface';

View File

@ -0,0 +1,40 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { TaxDefinition, TaxDefinitionCode } from "../../../domain";
import type { TaxDefinitionSummary } from "../../tax-definitions/models";
export interface ITaxDefinitionRepository {
create(taxDefinition: TaxDefinition, transaction?: unknown): Promise<Result<void, Error>>;
update(taxDefinition: TaxDefinition, transaction?: unknown): Promise<Result<void, Error>>;
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<boolean, Error>>;
existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<TaxDefinition, Error>>;
getByCodeInCompany(
companyId: UniqueID,
code: TaxDefinitionCode,
transaction?: unknown
): Promise<Result<TaxDefinition, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<TaxDefinitionSummary>, Error>>;
}

View File

@ -0,0 +1,5 @@
export * from "./tax-definition-creator.service";
export * from "./tax-definition-deleter.service";
export * from "./tax-definition-finder.service";
export * from "./tax-definition-status-changer.service";
export * from "./tax-definition-updater.service";

View File

@ -0,0 +1,37 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { type ITaxDefinitionCreateProps, TaxDefinition } from "../../../domain";
import type { ITaxDefinitionRepository } from "../repositories";
export interface ITaxDefinitionCreatorParams {
companyId: UniqueID;
id: UniqueID;
props: ITaxDefinitionCreateProps;
transaction: unknown;
}
export interface ITaxDefinitionCreator {
create(params: ITaxDefinitionCreatorParams): Promise<Result<TaxDefinition, Error>>;
}
export class TaxDefinitionCreator implements ITaxDefinitionCreator {
constructor(private readonly repository: ITaxDefinitionRepository) {}
public async create(params: ITaxDefinitionCreatorParams): Promise<Result<TaxDefinition, Error>> {
const { companyId, id, props, transaction } = params;
const taxDefinitionResult = TaxDefinition.create({ ...props, companyId }, id);
if (taxDefinitionResult.isFailure) {
return taxDefinitionResult;
}
const taxDefinition = taxDefinitionResult.data;
const saveResult = await this.repository.create(taxDefinition, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(taxDefinition);
}
}

View File

@ -0,0 +1,20 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionRepository } from "../repositories";
export interface ITaxDefinitionDeleter {
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
}
export class TaxDefinitionDeleter implements ITaxDefinitionDeleter {
constructor(private readonly repository: ITaxDefinitionRepository) {}
public async deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
return this.repository.deleteByIdInCompany(companyId, id, transaction);
}
}

View File

@ -0,0 +1,21 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { TaxDefinition } from "../../../domain";
import type { ITaxDefinitionRepository } from "../repositories";
export interface ITaxDefinitionFinder {
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<TaxDefinition, Error>>;
}
export class TaxDefinitionFinder implements ITaxDefinitionFinder {
constructor(private readonly repository: ITaxDefinitionRepository) {}
public async getByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
return this.repository.getByIdInCompany(companyId, id, transaction);
}
}

View File

@ -0,0 +1,49 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionRepository } from "../repositories";
export interface ITaxDefinitionStatusChanger {
enable(companyId: UniqueID, id: UniqueID, transaction?: unknown): Promise<Result<boolean, Error>>;
disable(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
}
export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger {
constructor(private readonly repository: ITaxDefinitionRepository) {}
public async enable(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
const existing = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existing.isFailure) return Result.fail(existing.error);
const taxDefinition = existing.data;
const changeResult = taxDefinition.enable();
if (changeResult.isFailure) return Result.fail(changeResult.error);
if (changeResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
}
return Result.ok(changeResult.data);
}
public async disable(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
const existing = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existing.isFailure) return Result.fail(existing.error);
const taxDefinition = existing.data;
const changeResult = taxDefinition.disable();
if (changeResult.isFailure) return Result.fail(changeResult.error);
if (changeResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
}
return Result.ok(changeResult.data);
}
}

View File

@ -0,0 +1,39 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { TaxDefinition, TaxDefinitionPatchProps } from "../../../domain";
import type { ITaxDefinitionRepository } from "../repositories";
export interface ITaxDefinitionUpdaterParams {
companyId: UniqueID;
id: UniqueID;
patch: TaxDefinitionPatchProps;
transaction: unknown;
}
export interface ITaxDefinitionUpdater {
update(params: ITaxDefinitionUpdaterParams): Promise<Result<TaxDefinition, Error>>;
}
export class TaxDefinitionUpdater implements ITaxDefinitionUpdater {
constructor(private readonly repository: ITaxDefinitionRepository) {}
public async update(params: ITaxDefinitionUpdaterParams): Promise<Result<TaxDefinition, Error>> {
const { companyId, id, patch, transaction } = params;
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) return Result.fail(existingResult.error);
const taxDefinition = existingResult.data;
const updateResult = taxDefinition.update(patch);
if (updateResult.isFailure) return Result.fail(updateResult.error);
if (updateResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
}
return Result.ok(taxDefinition);
}
}

View File

@ -0,0 +1,49 @@
import type { TaxDefinitionDetailDTO } from "@erp/catalogs/common";
import type { ISnapshotBuilder } from "@erp/core/api";
import type { TaxDefinitionDetail } from "../../models";
export interface ITaxDefinitionFullSnapshotBuilder
extends ISnapshotBuilder<TaxDefinitionDetail, TaxDefinitionDetailDTO> {}
export class TaxDefinitionFullSnapshotBuilder implements ITaxDefinitionFullSnapshotBuilder {
public toOutput(src: TaxDefinitionDetail): TaxDefinitionDetailDTO {
return {
id: src.id.toString(),
company_id: src.companyId.toString(),
code: src.code.toPrimitive(),
name: src.name.toPrimitive(),
description: src.description.match(
(v) => v.toPrimitive(),
() => null
),
rate: (src.rate as any).toPrimitive(),
tax_family: src.taxFamily,
calculation_behavior: src.calculationBehavior,
jurisdiction_country_code: src.jurisdictionCountryCode,
jurisdiction_region_code: src.jurisdictionRegionCode.match(
(v) => v,
() => null
),
tax_scope: src.taxScope,
invoice_note: src.invoiceNote.match(
(v) => v.toPrimitive(),
() => null
),
allowed_surcharge_codes: src.allowedSurchargeCodes.match(
(arr) => arr,
() => null
),
is_system: src.isSystem,
is_active: src.isActive,
valid_from: src.validFrom.match(
(d) => d.toPrimitive(),
() => null
),
valid_to: src.validTo.match(
(d) => d.toPrimitive(),
() => null
),
} as unknown as TaxDefinitionDetailDTO;
}
}

View File

@ -0,0 +1 @@
export * from "./full-tax-definition-snapshot.builder";

View File

@ -0,0 +1,2 @@
export * from "./full";
export * from "./summary";

View File

@ -0,0 +1 @@
export * from "./summary-tax-definition-snapshot.builder";

View File

@ -0,0 +1,20 @@
import type { TaxDefinitionSummaryDTO } from "@erp/catalogs/common";
import type { ISnapshotBuilder } from "@erp/core/api";
import type { TaxDefinitionSummary } from "../../models";
export interface ITaxDefinitionSummarySnapshotBuilder
extends ISnapshotBuilder<TaxDefinitionSummary, TaxDefinitionSummaryDTO> {}
export class TaxDefinitionSummarySnapshotBuilder implements ITaxDefinitionSummarySnapshotBuilder {
public toOutput(src: TaxDefinitionSummary): TaxDefinitionSummaryDTO {
return {
id: src.id.toString(),
company_id: src.companyId.toString(),
code: src.code.toPrimitive(),
name: src.name.toPrimitive(),
is_active: src.isActive,
is_system: src.isSystem,
};
}
}

View File

@ -0,0 +1,55 @@
import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common";
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ICreateTaxDefinitionInputMapper } from "../mappers";
import type { ITaxDefinitionCreator } from "../services";
import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders";
export type CreateTaxDefinitionUseCaseInput = {
companyId: UniqueID;
dto: CreateTaxDefinitionRequestDTO;
};
type CreateTaxDefinitionUseCaseDeps = {
dtoMapper: ICreateTaxDefinitionInputMapper;
creator: ITaxDefinitionCreator;
fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class CreateTaxDefinitionUseCase {
constructor(private readonly deps: CreateTaxDefinitionUseCaseDeps) {}
public execute(params: CreateTaxDefinitionUseCaseInput) {
const { dto, companyId } = params;
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) {
return mappedPropsResult;
}
const { props, id } = mappedPropsResult.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const createResult = await this.deps.creator.create({
companyId,
id,
props,
transaction,
});
if (createResult.isFailure) {
return createResult;
}
const snapshot = this.deps.fullSnapshotBuilder.toOutput(createResult.data as any);
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,19 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionDeleter } from "../services";
type DeleteTaxDefinitionDeps = {
deleter: ITaxDefinitionDeleter;
};
export class DeleteTaxDefinitionByIdUseCase {
constructor(private readonly deps: DeleteTaxDefinitionDeps) {}
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.deleter.deleteByIdInCompany(companyId, id);
if (result.isFailure) return result;
return Result.ok(result.data);
}
}

View File

@ -0,0 +1,19 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionStatusChanger } from "../services";
type DisableTaxDefinitionDeps = {
statusChanger: ITaxDefinitionStatusChanger;
};
export class DisableTaxDefinitionByIdUseCase {
constructor(private readonly deps: DisableTaxDefinitionDeps) {}
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.statusChanger.disable(companyId, id);
if (result.isFailure) return result;
return Result.ok(result.data);
}
}

View File

@ -0,0 +1,19 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionStatusChanger } from "../services";
type EnableTaxDefinitionDeps = {
statusChanger: ITaxDefinitionStatusChanger;
};
export class EnableTaxDefinitionByIdUseCase {
constructor(private readonly deps: EnableTaxDefinitionDeps) {}
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.statusChanger.enable(companyId, id);
if (result.isFailure) return result;
return Result.ok(result.data);
}
}

View File

@ -0,0 +1,22 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionFinder } from "../services";
import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders";
type GetTaxDefinitionByIdUseCaseDeps = {
finder: ITaxDefinitionFinder;
fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder;
};
export class GetTaxDefinitionByIdUseCase {
constructor(private readonly deps: GetTaxDefinitionByIdUseCaseDeps) {}
public async execute(companyId: UniqueID, id: UniqueID) {
const found = await this.deps.finder.getByIdInCompany(companyId, id);
if (found.isFailure) return found;
const snapshot = this.deps.fullSnapshotBuilder.toOutput(found.data as any);
return Result.ok(snapshot);
}
}

View File

@ -0,0 +1,7 @@
export * from "./create-tax-definition.use-case";
export * from "./delete-tax-definition-by-id.use-case";
export * from "./disable-tax-definition-by-id.use-case";
export * from "./enable-tax-definition-by-id.use-case";
export * from "./get-tax-definition-by-id.use-case";
export * from "./list-tax-definitions.use-case";
export * from "./update-tax-definition-by-id.use-case";

View File

@ -0,0 +1,23 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionRepository } from "../repositories";
import type { ITaxDefinitionSummarySnapshotBuilder } from "../snapshot-builders";
type ListTaxDefinitionsUseCaseDeps = {
repository: ITaxDefinitionRepository;
summarySnapshotBuilder: ITaxDefinitionSummarySnapshotBuilder;
};
export class ListTaxDefinitionsUseCase {
constructor(private readonly deps: ListTaxDefinitionsUseCaseDeps) {}
public async execute(companyId: UniqueID, criteria: Criteria) {
const found = await this.deps.repository.findByCriteriaInCompany(companyId, criteria);
if (found.isFailure) return found;
const collection = found.data.map((item) => this.deps.summarySnapshotBuilder.toOutput(item));
return Result.ok(collection);
}
}

View File

@ -0,0 +1,37 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IUpdateTaxDefinitionByIdInputMapper } from "../mappers";
import type { ITaxDefinitionUpdater } from "../services";
import type { ITaxDefinitionFullSnapshotBuilder } from "../snapshot-builders";
type UpdateTaxDefinitionByIdDeps = {
dtoMapper: IUpdateTaxDefinitionByIdInputMapper;
updater: ITaxDefinitionUpdater;
fullSnapshotBuilder: ITaxDefinitionFullSnapshotBuilder;
};
export type UpdateTaxDefinitionByIdUseCaseInput = {
id: UniqueID;
dto: unknown;
};
export class UpdateTaxDefinitionByIdUseCase {
constructor(private readonly deps: UpdateTaxDefinitionByIdDeps) {}
public async execute(companyId: UniqueID, params: UpdateTaxDefinitionByIdUseCaseInput) {
const mapped = this.deps.dtoMapper.map(params.dto as any);
if (mapped.isFailure) return mapped;
const result = await this.deps.updater.update({
companyId,
id: params.id,
patch: mapped.data,
transaction: undefined,
});
if (result.isFailure) return result;
const snapshot = this.deps.fullSnapshotBuilder.toOutput(result.data as any);
return Result.ok(snapshot);
}
}

View File

@ -1,3 +1,4 @@
export * from "./payment-methods";
export * from "./payment-terms";
export * from "./tax-definitions";
export * from "./tax-regimes";

View File

@ -0,0 +1,34 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionCalculationBehaviorError } from "./errors";
export type TaxCalculationBehaviorType = "additive" | "subtractive" | "neutral";
export class TaxCalculationBehavior {
private constructor(private readonly value: TaxCalculationBehaviorType) {}
public static create(value: string): Result<TaxCalculationBehavior, Error> {
const v = value?.toString() ?? "";
const allowed: TaxCalculationBehaviorType[] = ["additive", "subtractive", "neutral"];
if (!allowed.includes(v as TaxCalculationBehaviorType)) {
return Result.fail(
new InvalidTaxDefinitionCalculationBehaviorError("Invalid calculation behavior")
);
}
return Result.ok(new TaxCalculationBehavior(v as TaxCalculationBehaviorType));
}
public static fromPersistence(value: string): TaxCalculationBehavior {
return new TaxCalculationBehavior(value as TaxCalculationBehaviorType);
}
public toPrimitive(): TaxCalculationBehaviorType {
return this.value;
}
public toString(): string {
return this.value;
}
}

View File

@ -0,0 +1,129 @@
import { DomainError } from "@repo/rdx-ddd";
export class InvalidTaxDefinitionIdError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_ID" as const;
}
export const isInvalidTaxDefinitionIdError = (e: unknown): e is InvalidTaxDefinitionIdError =>
e instanceof InvalidTaxDefinitionIdError;
export class InvalidTaxDefinitionCompanyIdError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_COMPANY_ID" as const;
}
export const isInvalidTaxDefinitionCompanyIdError = (
e: unknown
): e is InvalidTaxDefinitionCompanyIdError => e instanceof InvalidTaxDefinitionCompanyIdError;
export class InvalidTaxDefinitionCodeError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_CODE" as const;
}
export const isInvalidTaxDefinitionCodeError = (e: unknown): e is InvalidTaxDefinitionCodeError =>
e instanceof InvalidTaxDefinitionCodeError;
export class InvalidTaxDefinitionNameError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_NAME" as const;
}
export const isInvalidTaxDefinitionNameError = (e: unknown): e is InvalidTaxDefinitionNameError =>
e instanceof InvalidTaxDefinitionNameError;
export class InvalidTaxDefinitionDescriptionError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_DESCRIPTION" as const;
}
export const isInvalidTaxDefinitionDescriptionError = (
e: unknown
): e is InvalidTaxDefinitionDescriptionError => e instanceof InvalidTaxDefinitionDescriptionError;
export class InvalidTaxDefinitionRateError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_RATE" as const;
}
export const isInvalidTaxDefinitionRateError = (e: unknown): e is InvalidTaxDefinitionRateError =>
e instanceof InvalidTaxDefinitionRateError;
export class InvalidTaxDefinitionFamilyError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_FAMILY" as const;
}
export const isInvalidTaxDefinitionFamilyError = (
e: unknown
): e is InvalidTaxDefinitionFamilyError => e instanceof InvalidTaxDefinitionFamilyError;
export class InvalidTaxDefinitionCalculationBehaviorError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_CALCULATION_BEHAVIOR" as const;
}
export const isInvalidTaxDefinitionCalculationBehaviorError = (
e: unknown
): e is InvalidTaxDefinitionCalculationBehaviorError =>
e instanceof InvalidTaxDefinitionCalculationBehaviorError;
export class InvalidTaxDefinitionJurisdictionCountryCodeError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_COUNTRY_CODE" as const;
}
export const isInvalidTaxDefinitionJurisdictionCountryCodeError = (
e: unknown
): e is InvalidTaxDefinitionJurisdictionCountryCodeError =>
e instanceof InvalidTaxDefinitionJurisdictionCountryCodeError;
export class InvalidTaxDefinitionJurisdictionRegionCodeError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_JURISDICTION_REGION_CODE" as const;
}
export const isInvalidTaxDefinitionJurisdictionRegionCodeError = (
e: unknown
): e is InvalidTaxDefinitionJurisdictionRegionCodeError =>
e instanceof InvalidTaxDefinitionJurisdictionRegionCodeError;
export class InvalidTaxDefinitionScopeError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_SCOPE" as const;
}
export const isInvalidTaxDefinitionScopeError = (e: unknown): e is InvalidTaxDefinitionScopeError =>
e instanceof InvalidTaxDefinitionScopeError;
export class InvalidTaxDefinitionInvoiceNoteError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_INVOICE_NOTE" as const;
}
export const isInvalidTaxDefinitionInvoiceNoteError = (
e: unknown
): e is InvalidTaxDefinitionInvoiceNoteError => e instanceof InvalidTaxDefinitionInvoiceNoteError;
export class InvalidTaxDefinitionAllowedSurchargeCodesError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_ALLOWED_SURCHARGE_CODES" as const;
}
export const isInvalidTaxDefinitionAllowedSurchargeCodesError = (
e: unknown
): e is InvalidTaxDefinitionAllowedSurchargeCodesError =>
e instanceof InvalidTaxDefinitionAllowedSurchargeCodesError;
export class InvalidTaxDefinitionValidityPeriodError extends DomainError {
public readonly code = "TAX_DEFINITION_INVALID_VALIDITY_PERIOD" as const;
}
export const isInvalidTaxDefinitionValidityPeriodError = (
e: unknown
): e is InvalidTaxDefinitionValidityPeriodError =>
e instanceof InvalidTaxDefinitionValidityPeriodError;
export class TaxDefinitionCannotBeEnabledError extends DomainError {
public readonly code = "TAX_DEFINITION_CANNOT_BE_ENABLED" as const;
}
export const isTaxDefinitionCannotBeEnabledError = (
e: unknown
): e is TaxDefinitionCannotBeEnabledError => e instanceof TaxDefinitionCannotBeEnabledError;
export class TaxDefinitionCannotBeDisabledError extends DomainError {
public readonly code = "TAX_DEFINITION_CANNOT_BE_DISABLED" as const;
}
export const isTaxDefinitionCannotBeDisabledError = (
e: unknown
): e is TaxDefinitionCannotBeDisabledError => e instanceof TaxDefinitionCannotBeDisabledError;

View File

@ -0,0 +1,10 @@
export * from "./calculation-behavior";
export * from "./errors";
export * from "./jurisdiction-country-code";
export * from "./jurisdiction-region-code";
export * from "./tax-definition.aggregate";
export * from "./tax-definition-code";
export * from "./tax-definition-name";
export * from "./tax-family";
export * from "./tax-rate";
export * from "./tax-scope";

View File

@ -0,0 +1,41 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionJurisdictionCountryCodeError } from "./errors";
export class TaxJurisdictionCountryCode {
private constructor(private readonly value: string) {}
public static create(code: string): Result<TaxJurisdictionCountryCode, Error> {
const trimmed = code?.trim() ?? "";
if (trimmed.length !== 2) {
return Result.fail(
new InvalidTaxDefinitionJurisdictionCountryCodeError("Country code must be 2 letters")
);
}
const normalized = trimmed.toUpperCase();
const regex = /^[A-Z]{2}$/;
if (!regex.test(normalized)) {
return Result.fail(
new InvalidTaxDefinitionJurisdictionCountryCodeError(
"Country code must be 2 uppercase letters"
)
);
}
// NOTE: We allow two-letter codes including 'EU' to support union-level definitions like reverse charge.
return Result.ok(new TaxJurisdictionCountryCode(normalized));
}
public static fromPersistence(code: string): TaxJurisdictionCountryCode {
return new TaxJurisdictionCountryCode(code);
}
public toPrimitive(): string {
return this.value;
}
public toString(): string {
return this.value;
}
}

View File

@ -0,0 +1,42 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionJurisdictionRegionCodeError } from "./errors";
export class TaxJurisdictionRegionCode {
private constructor(private readonly value: string) {}
public static create(code: string): Result<TaxJurisdictionRegionCode, Error> {
const trimmed = code?.trim() ?? "";
if (trimmed.length === 0) {
return Result.fail(
new InvalidTaxDefinitionJurisdictionRegionCodeError("Region code cannot be empty")
);
}
const normalized = trimmed.toUpperCase();
// basic pattern: CC-... (we won't validate full ISO-3166-2 list)
const regex = /^[A-Z]{2}-[A-Z0-9-]+$/;
if (!regex.test(normalized)) {
return Result.fail(
new InvalidTaxDefinitionJurisdictionRegionCodeError(
"Region code must follow ISO-3166-2 pattern COUNTRY-REGION"
)
);
}
return Result.ok(new TaxJurisdictionRegionCode(normalized));
}
public static fromPersistence(code: string): TaxJurisdictionRegionCode {
return new TaxJurisdictionRegionCode(code);
}
public toPrimitive(): string {
return this.value;
}
public toString(): string {
return this.value;
}
}

View File

@ -0,0 +1,36 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionCodeError } from "./errors";
export class TaxDefinitionCode {
private constructor(private readonly value: string) {}
public static create(code: string): Result<TaxDefinitionCode, Error> {
const trimmed = code?.trim() ?? "";
if (trimmed.length === 0) {
return Result.fail(new InvalidTaxDefinitionCodeError("Tax definition code cannot be empty"));
}
const normalized = trimmed.toLowerCase();
const regex = /^[a-z0-9][a-z0-9_]*$/;
if (!regex.test(normalized)) {
return Result.fail(
new InvalidTaxDefinitionCodeError("Tax definition code must match ^[a-z0-9][a-z0-9_]*$")
);
}
return Result.ok(new TaxDefinitionCode(normalized));
}
public static fromPersistence(code: string): TaxDefinitionCode {
return new TaxDefinitionCode(code);
}
public toString(): string {
return this.value;
}
public toPrimitive(): string {
return this.value;
}
}

View File

@ -0,0 +1,29 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionNameError } from "./errors";
export class TaxDefinitionName {
private constructor(private readonly value: string) {}
public static create(name: string): Result<TaxDefinitionName, Error> {
const trimmed = name?.trim() ?? "";
if (trimmed.length === 0) {
return Result.fail(new InvalidTaxDefinitionNameError("Tax definition name cannot be empty"));
}
// follow existing patterns: no extra length validation here
return Result.ok(new TaxDefinitionName(trimmed));
}
public static fromPersistence(name: string): TaxDefinitionName {
return new TaxDefinitionName(name);
}
public toString(): string {
return this.value;
}
public toPrimitive(): string {
return this.value;
}
}

View File

@ -0,0 +1,369 @@
import { AggregateRoot, type TextValue, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { TaxCalculationBehavior } from "./calculation-behavior";
import {
InvalidTaxDefinitionAllowedSurchargeCodesError,
InvalidTaxDefinitionJurisdictionRegionCodeError,
InvalidTaxDefinitionValidityPeriodError,
} from "./errors";
import type { TaxJurisdictionCountryCode } from "./jurisdiction-country-code";
import type { TaxJurisdictionRegionCode } from "./jurisdiction-region-code";
import type { TaxDefinitionCode } from "./tax-definition-code";
import type { TaxDefinitionName } from "./tax-definition-name";
import type { TaxFamily } from "./tax-family";
import type { TaxRate } from "./tax-rate";
import type { TaxScope } from "./tax-scope";
export interface ITaxDefinitionCreateProps {
companyId: UniqueID;
code: TaxDefinitionCode;
name: TaxDefinitionName;
description: Maybe<TextValue>;
rate: TaxRate;
taxFamily: TaxFamily;
calculationBehavior: TaxCalculationBehavior;
jurisdictionCountryCode: TaxJurisdictionCountryCode;
jurisdictionRegionCode: Maybe<TaxJurisdictionRegionCode>;
taxScope: TaxScope;
invoiceNote: Maybe<TextValue>; // Texto fiscal que aparece en el documento
allowedSurchargeCodes: Maybe<TaxDefinitionCode[]>;
isSystem: boolean;
isActive: boolean;
validFrom: Maybe<UtcDate>;
validTo: Maybe<UtcDate>;
}
export type TaxDefinitionPatchProps = Partial<{
name: TaxDefinitionName;
description: Maybe<TextValue>;
rate: TaxRate;
invoiceNote: Maybe<TextValue>;
allowedSurchargeCodes: Maybe<TaxDefinitionCode[]>;
isActive: boolean;
validFrom: Maybe<UtcDate>;
validTo: Maybe<UtcDate>;
}>;
export type TaxDefinitionInternalProps = ITaxDefinitionCreateProps;
export class TaxDefinition extends AggregateRoot<TaxDefinitionInternalProps> {
protected constructor(props: TaxDefinitionInternalProps, id?: UniqueID) {
super(props, id);
}
public static create(
props: ITaxDefinitionCreateProps,
id?: UniqueID
): Result<TaxDefinition, Error> {
const validationResult = TaxDefinition.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const taxDefinition = new TaxDefinition(props, id);
return Result.ok(taxDefinition);
}
public static rehydrate(props: TaxDefinitionInternalProps, id: UniqueID): TaxDefinition {
return new TaxDefinition(props, id);
}
private static validateCreateProps(props: ITaxDefinitionCreateProps): Result<void, Error> {
if (!props.companyId) {
return Result.fail(new Error("Tax definition company ID is required"));
}
if (!props.code) {
return Result.fail(new Error("Tax definition code is required"));
}
if (!props.name) {
return Result.fail(new Error("Tax definition name is required"));
}
if (!props.rate) {
return Result.fail(new Error("Tax definition rate is required"));
}
if (!props.taxFamily) {
return Result.fail(new Error("Tax definition family is required"));
}
if (!props.calculationBehavior) {
return Result.fail(new Error("Tax definition calculation behavior is required"));
}
if (!props.jurisdictionCountryCode) {
return Result.fail(new Error("Tax definition jurisdiction country code is required"));
}
if (!props.taxScope) {
return Result.fail(new Error("Tax definition scope is required"));
}
// allowedSurchargeCodes rules
if (props.allowedSurchargeCodes && props.allowedSurchargeCodes.isSome()) {
const arr = props.allowedSurchargeCodes.unwrap();
// duplicates
const primitives = arr.map((c) => c.toPrimitive());
const unique = Array.from(new Set(primitives));
if (unique.length !== primitives.length) {
return Result.fail(
new InvalidTaxDefinitionAllowedSurchargeCodesError("Duplicate surcharge codes")
);
}
// cannot reference itself
if (primitives.includes(props.code.toPrimitive())) {
return Result.fail(
new InvalidTaxDefinitionAllowedSurchargeCodesError(
"Surcharge codes cannot contain the tax's own code"
)
);
}
// only allowed for 'iva'
if (props.taxFamily.toPrimitive() !== "iva") {
return Result.fail(
new InvalidTaxDefinitionAllowedSurchargeCodesError(
"Allowed surcharge codes only valid for 'iva' family"
)
);
}
}
// validFrom/validTo coherence
if (props.validFrom && props.validFrom.isSome() && props.validTo && props.validTo.isSome()) {
const from = props.validFrom.unwrap();
const to = props.validTo.unwrap();
if (from.toPrimitive() > to.toPrimitive()) {
return Result.fail(
new InvalidTaxDefinitionValidityPeriodError("validFrom must be <= validTo")
);
}
}
// region coherence with country
if (props.jurisdictionRegionCode && props.jurisdictionRegionCode.isSome()) {
const region = props.jurisdictionRegionCode.unwrap().toPrimitive();
const country = props.jurisdictionCountryCode.toPrimitive();
if (!region.startsWith(`${country}-`)) {
return Result.fail(
new InvalidTaxDefinitionJurisdictionRegionCodeError(
"Region code must start with country code"
)
);
}
}
return Result.ok();
}
private static validatePatchProps(patchProps: TaxDefinitionPatchProps): Result<void, Error> {
if (Object.keys(patchProps).length === 0) {
return Result.ok();
}
return Result.ok();
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get code(): TaxDefinitionCode {
return this.props.code;
}
public get name(): TaxDefinitionName {
return this.props.name;
}
public get description(): Maybe<TextValue> {
return this.props.description;
}
public get rate(): TaxRate {
return this.props.rate as TaxRate;
}
public get taxFamily(): TaxFamily {
return this.props.taxFamily;
}
public get calculationBehavior(): TaxCalculationBehavior {
return this.props.calculationBehavior;
}
public get jurisdictionCountryCode(): TaxJurisdictionCountryCode {
return this.props.jurisdictionCountryCode;
}
public get jurisdictionRegionCode(): Maybe<TaxJurisdictionRegionCode> {
return this.props.jurisdictionRegionCode;
}
public get taxScope(): TaxScope {
return this.props.taxScope;
}
public get invoiceNote(): Maybe<TextValue> {
return this.props.invoiceNote;
}
public get allowedSurchargeCodes(): Maybe<TaxDefinitionCode[]> {
return this.props.allowedSurchargeCodes;
}
public get isSystem(): boolean {
return this.props.isSystem;
}
public get isActive(): boolean {
return this.props.isActive;
}
public get validFrom(): Maybe<UtcDate> {
return this.props.validFrom;
}
public get validTo(): Maybe<UtcDate> {
return this.props.validTo;
}
public update(patchProps: TaxDefinitionPatchProps): Result<boolean, Error> {
const validationResult = TaxDefinition.validatePatchProps(patchProps);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
let hasChanges = false;
// Only allow updating mutable fields
if (
patchProps.name !== undefined &&
this.props.name.toPrimitive() !== patchProps.name.toPrimitive()
) {
this.props.name = patchProps.name;
hasChanges = true;
}
if (
patchProps.description !== undefined &&
!TaxDefinition.sameDescription(this.props.description, patchProps.description)
) {
this.props.description = patchProps.description;
hasChanges = true;
}
if (
patchProps.rate !== undefined &&
this.props.rate.toPrimitive() !== patchProps.rate.toPrimitive()
) {
this.props.rate = patchProps.rate as unknown as any;
hasChanges = true;
}
if (
patchProps.invoiceNote !== undefined &&
!TaxDefinition.sameDescription(this.props.invoiceNote, patchProps.invoiceNote)
) {
this.props.invoiceNote = patchProps.invoiceNote;
hasChanges = true;
}
if (patchProps.allowedSurchargeCodes !== undefined) {
this.props.allowedSurchargeCodes = patchProps.allowedSurchargeCodes;
hasChanges = true;
}
if (patchProps.isActive !== undefined && this.props.isActive !== patchProps.isActive) {
this.props.isActive = patchProps.isActive;
hasChanges = true;
}
if (patchProps.validFrom !== undefined) {
this.props.validFrom = patchProps.validFrom;
hasChanges = true;
}
if (patchProps.validTo !== undefined) {
this.props.validTo = patchProps.validTo;
hasChanges = true;
}
return Result.ok(hasChanges);
}
public disable(): Result<boolean, Error> {
if (!this.isActive) {
return Result.ok(false);
}
this.props.isActive = false;
return Result.ok(true);
}
public enable(): Result<boolean, Error> {
if (this.isActive) {
return Result.ok(false);
}
this.props.isActive = true;
return Result.ok(true);
}
public toJSON() {
return {
id: this.id.toPrimitive(),
company_id: this.companyId.toPrimitive(),
code: this.code.toPrimitive(),
name: this.name.toPrimitive(),
description: this.description.match(
(v) => v.toPrimitive(),
() => null
),
rate: { value: this.rate.toPrimitive(), scale: (this.rate as any).scale ?? 2 },
tax_family: this.taxFamily.toPrimitive(),
calculation_behavior: this.calculationBehavior.toPrimitive(),
jurisdiction_country_code: this.jurisdictionCountryCode.toPrimitive(),
jurisdiction_region_code: this.jurisdictionRegionCode.match(
(v) => v.toPrimitive(),
() => null
),
tax_scope: this.taxScope.toPrimitive(),
invoice_note: this.invoiceNote.match(
(v) => v.toPrimitive(),
() => null
),
allowed_surcharge_codes: this.allowedSurchargeCodes.match(
(arr) => arr.map((c) => c.toPrimitive()),
() => null
),
is_system: this.isSystem,
is_active: this.isActive,
valid_from: this.validFrom.match(
(d) => d.toPrimitive(),
() => null
),
valid_to: this.validTo.match(
(d) => d.toPrimitive(),
() => null
),
};
}
private static sameDescription(current: Maybe<TextValue>, next: Maybe<TextValue>): boolean {
return current.match(
(currentValue) =>
next.match(
(nextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(),
() => false
),
() => next.isNone()
);
}
}

View File

@ -0,0 +1,57 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionFamilyError } from "./errors";
export type TaxFamilyType =
| "iva"
| "igic"
| "ipsi"
| "equivalence_surcharge"
| "withholding"
| "vat"
| "gst"
| "sales_tax"
| "reverse_charge"
| "exempt"
| "not_subject"
| "custom";
export class TaxFamily {
private constructor(private readonly value: TaxFamilyType) {}
public static create(value: string): Result<TaxFamily, Error> {
const v = value?.toString() ?? "";
const allowed: TaxFamilyType[] = [
"iva",
"igic",
"ipsi",
"equivalence_surcharge",
"withholding",
"vat",
"gst",
"sales_tax",
"reverse_charge",
"exempt",
"not_subject",
"custom",
];
if (!allowed.includes(v as TaxFamilyType)) {
return Result.fail(new InvalidTaxDefinitionFamilyError("Invalid tax family"));
}
return Result.ok(new TaxFamily(v as TaxFamilyType));
}
public static fromPersistence(value: string): TaxFamily {
return new TaxFamily(value as TaxFamilyType);
}
public toPrimitive(): TaxFamilyType {
return this.value;
}
public toString(): string {
return this.value;
}
}

View File

@ -0,0 +1,40 @@
import { Percentage, type PercentageProps, ValidationErrorCollection } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionRateError } from "./errors";
type TaxRateProps = PercentageProps;
export class TaxRate extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value, scale }: TaxRateProps): Result<Percentage, Error> {
if (scale && scale !== TaxRate.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `TaxRate scale must be ${TaxRate.DEFAULT_SCALE}` },
])
);
}
// Basic range validation: 0.00 .. 100.00 (value expressed with scale 2 => 0 .. 10000)
if (value < 0) {
return Result.fail(new InvalidTaxDefinitionRateError("Tax rate must be >= 0"));
}
if (value > 100 * 10 ** TaxRate.DEFAULT_SCALE) {
return Result.fail(new InvalidTaxDefinitionRateError("Tax rate must be <= 100.00"));
}
return Result.ok(
new TaxRate({
value,
scale: TaxRate.DEFAULT_SCALE,
})
);
}
static zero() {
return TaxRate.create({ value: 0 }).data;
}
}

View File

@ -0,0 +1,32 @@
import { Result } from "@repo/rdx-utils";
import { InvalidTaxDefinitionScopeError } from "./errors";
export type TaxScopeType = "domestic" | "intra_eu" | "export" | "import" | "international";
export class TaxScope {
private constructor(private readonly value: TaxScopeType) {}
public static create(value: string): Result<TaxScope, Error> {
const v = value?.toString() ?? "";
const allowed: TaxScopeType[] = ["domestic", "intra_eu", "export", "import", "international"];
if (!allowed.includes(v as TaxScopeType)) {
return Result.fail(new InvalidTaxDefinitionScopeError("Invalid tax scope"));
}
return Result.ok(new TaxScope(v as TaxScopeType));
}
public static fromPersistence(value: string): TaxScope {
return new TaxScope(value as TaxScopeType);
}
public toPrimitive(): TaxScopeType {
return this.value;
}
public toString(): string {
return this.value;
}
}

View File

@ -13,35 +13,62 @@ import {
buildCatalogsPublicServices,
} from "./infrastructure/di/catalogs.di";
export * from "./infrastructure/payment-methods/persistence/sequelize";
export * from "./application/services/catalog-public-services.interface"; // <- exportamos la interfaz de los servicios públicos para que otros módulos puedan usarla en sus dependencias
//export * from "./infrastructure/payment-methods/persistence/sequelize"; <- ???
export const catalogsAPIModule: IModuleServer = {
name: "catalogs",
version: "1.0.0",
dependencies: [],
/**
* Fase de SETUP
* ----------------
* - Construye el dominio (una sola vez)
* - Define qué expone el módulo
* - NO conecta infraestructura
*/
async setup(params) {
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
// 1) Dominio interno
const internal = buildCatalogsDependencies(params);
// 2) Servicios públicos (Application Services)
const publicServices = buildCatalogsPublicServices(params, internal);
params.logger.info("🚀 Catalogs module dependencies registered", {
logger.info("🚀 Catalogs module dependencies registered", {
label: this.name,
});
return {
// Modelos Sequelize del módulo
models: [...paymentMethodModels, ...paymentTermModels, ...taxRegimeModels],
// Servicios expuestos a otros módulos
services: {
paymentMethod: publicServices.paymentMethods,
paymentMethods: publicServices.paymentMethods,
paymentTerms: publicServices.paymentTerms,
taxRegimes: publicServices.taxRegimes,
},
// Implementación privada del módulo
internal,
};
},
/**
* Fase de START
* -------------
* - Conecta el módulo al runtime
* - Puede usar servicios e internals ya construidos
* - NO construye dominio
*/
async start(params) {
const { logger } = params;
// Registro de rutas HTTP
paymentMethodsRouter(params);
paymentTermsRouter(params);
taxRegimesRouter(params);
@ -50,6 +77,14 @@ export const catalogsAPIModule: IModuleServer = {
label: this.name,
});
},
/**
* Warmup opcional (si lo necesitas en el futuro)
* ----------------------------------------------
* warmup(params) {
* ...
* }
*/
};
export default catalogsAPIModule;

View File

@ -1,3 +1,4 @@
export * from "./payment-methods";
export * from "./payment-terms";
export * from "./tax-definitions";
export * from "./tax-regimes";

View File

@ -0,0 +1,2 @@
export * from "./tax-definition-persistence-mappers.di";
export * from "./tax-definition-repositories.di";

View File

@ -0,0 +1,16 @@
import {
SequelizeTaxDefinitionDomainMapper,
SequelizeTaxDefinitionSummaryMapper,
} from "../persistence";
export interface ITaxDefinitionPersistenceMappers {
domainMapper: SequelizeTaxDefinitionDomainMapper;
listMapper: SequelizeTaxDefinitionSummaryMapper;
}
export const buildTaxDefinitionPersistenceMappers = (): ITaxDefinitionPersistenceMappers => {
const domainMapper = new SequelizeTaxDefinitionDomainMapper();
const listMapper = new SequelizeTaxDefinitionSummaryMapper();
return { domainMapper, listMapper };
};

View File

@ -0,0 +1,13 @@
import type { Sequelize } from "sequelize";
import { SequelizeTaxDefinitionRepository } from "../persistence";
import type { ITaxDefinitionPersistenceMappers } from "./tax-definition-persistence-mappers.di";
export const buildTaxDefinitionRepository = (params: {
database: Sequelize;
mappers: ITaxDefinitionPersistenceMappers;
}) => {
const { database, mappers } = params;
return new SequelizeTaxDefinitionRepository(mappers.domainMapper, mappers.listMapper, database);
};

View File

@ -0,0 +1,2 @@
export * from "./di";
export * from "./persistence";

View File

@ -0,0 +1 @@
export * from "./sequelize";

View File

@ -0,0 +1,7 @@
export * from "./mappers";
export * from "./models";
export * from "./repositories";
import taxDefinitionModelInit from "./models/sequelize-tax-definition.model";
export const taxDefinitionModels = [taxDefinitionModelInit];

View File

@ -0,0 +1,2 @@
export * from "./sequelize-tax-definition-domain.mapper";
export * from "./sequelize-tax-definition-summary.mapper";

View File

@ -0,0 +1,157 @@
import { SequelizeQueryMapper } from "@erp/core/api";
import {
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
TaxCalculationBehavior,
TaxDefinition,
TaxDefinitionCode as TaxDefinitionCodeVO,
TaxDefinitionName as TaxDefinitionNameVO,
TaxFamily,
TaxJurisdictionCountryCode,
TaxJurisdictionRegionCode,
TaxRate as TaxRateVO,
TaxScope,
} from "../../../../../domain";
import type { TaxDefinitionModel } from "../models";
export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
TaxDefinitionModel,
TaxDefinition
> {
public mapToDomain(raw: TaxDefinitionModel): Result<TaxDefinition, Error> {
const errors: ValidationErrorDetail[] = [];
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const code = extractOrPushError(TaxDefinitionCodeVO.create(raw.code), "code", errors);
const name = extractOrPushError(TaxDefinitionNameVO.create(raw.name), "name", errors);
const description = maybeFromNullableResult(raw.description, (v) => TextValue.create(v));
const rate = extractOrPushError(
TaxRateVO.create({ value: raw.rate_value, scale: raw.rate_scale }),
"rate",
errors
);
const taxFamily = extractOrPushError(TaxFamily.create(raw.tax_family), "tax_family", errors);
const calculationBehavior = extractOrPushError(
TaxCalculationBehavior.create(raw.calculation_behavior),
"calculation_behavior",
errors
);
const jurisdictionCountryCode = extractOrPushError(
TaxJurisdictionCountryCode.create(raw.jurisdiction_country_code),
"jurisdiction_country_code",
errors
);
const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) =>
TaxJurisdictionRegionCode.create(v)
);
const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors);
const invoiceNote = maybeFromNullableResult(raw.invoice_note, (v) => TextValue.create(v));
const allowedSurchargeCodes = maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => {
if (!Array.isArray(v)) return Result.fail(new Error("Invalid allowed_surcharge_codes"));
const arr: any[] = [];
for (const el of v) {
const r = TaxDefinitionCodeVO.create(String(el));
if (r.isFailure) return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
arr.push(r.getValue());
}
return Result.ok(arr);
});
// valid from / to
const validFrom = maybeFromNullableResult(raw.valid_from, (v) => Result.ok(String(v)));
const validTo = maybeFromNullableResult(raw.valid_to, (v) => Result.ok(String(v)));
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("TaxDefinition mapping failed [mapToDomain]", errors)
);
}
const domainOrError = TaxDefinition.rehydrate(
{
companyId: companyId!,
code: code!,
name: name!,
description: description,
rate: rate!,
taxFamily: taxFamily!,
calculationBehavior: calculationBehavior!,
jurisdictionCountryCode: jurisdictionCountryCode!,
jurisdictionRegionCode: jurisdictionRegionCode,
taxScope: taxScope!,
invoiceNote: invoiceNote,
allowedSurchargeCodes: allowedSurchargeCodes,
isSystem: raw.is_system,
isActive: raw.is_active,
validFrom: validFrom,
validTo: validTo,
} as any,
id!
);
return Result.ok(domainOrError);
}
public mapToPersistence(domain: TaxDefinition): Result<Record<string, unknown>, Error> {
const dto: Record<string, unknown> = {
id: domain.id.toPrimitive(),
company_id: domain.companyId.toPrimitive(),
code: domain.code.toPrimitive(),
name: domain.name.toPrimitive(),
description: domain.description.match(
(v) => v.toPrimitive(),
() => null
),
rate_value: domain.rate.toPrimitive(),
rate_scale: (domain.rate as any).scale ?? 2,
tax_family: domain.taxFamily.toPrimitive(),
calculation_behavior: domain.calculationBehavior.toPrimitive(),
jurisdiction_country_code: domain.jurisdictionCountryCode.toPrimitive(),
jurisdiction_region_code: domain.jurisdictionRegionCode.match(
(v) => v.toPrimitive(),
() => null
),
tax_scope: domain.taxScope.toPrimitive(),
invoice_note: domain.invoiceNote.match(
(v) => v.toPrimitive(),
() => null
),
allowed_surcharge_codes: domain.allowedSurchargeCodes.match(
(arr) => arr.map((c) => c.toPrimitive()),
() => null
),
is_system: domain.isSystem,
is_active: domain.isActive,
valid_from: domain.validFrom.match(
(d) => d.toPrimitive(),
() => null
),
valid_to: domain.validTo.match(
(d) => d.toPrimitive(),
() => null
),
};
return Result.ok(dto);
}
}

View File

@ -0,0 +1,48 @@
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { TaxDefinitionSummary } from "../../../../../application";
import { TaxDefinitionCode } from "../../../../../domain";
import type { TaxDefinitionModel } from "../models";
export class SequelizeTaxDefinitionSummaryMapper extends SequelizeQueryMapper<
TaxDefinitionModel,
TaxDefinitionSummary
> {
public mapToReadModel(
raw: TaxDefinitionModel,
_params?: MapperParamsType
): Result<TaxDefinitionSummary, Error> {
const errors: ValidationErrorDetail[] = [];
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const code = extractOrPushError(TaxDefinitionCode.create(raw.code), "code", errors);
const name = extractOrPushError(TextValue.create(raw.name), "name", errors);
const isActive = raw.is_active;
const isSystem = raw.is_system;
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("TaxDefinition mapping failed [mapToDTO]", errors)
);
}
return Result.ok<TaxDefinitionSummary>({
id: id!,
companyId: companyId!,
code: code!,
name: name!,
isActive,
isSystem,
});
}
}

View File

@ -0,0 +1,171 @@
import {
type CreationOptional,
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type Sequelize,
} from "sequelize";
export type TaxDefinitionCreationAttributes = InferCreationAttributes<TaxDefinitionModel, {}> & {};
export class TaxDefinitionModel extends Model<
InferAttributes<TaxDefinitionModel>,
InferCreationAttributes<TaxDefinitionModel>
> {
declare id: string;
declare company_id: string;
declare code: string;
declare name: string;
declare description: CreationOptional<string | null>;
declare rate_value: number;
declare rate_scale: number;
declare tax_family: string;
declare calculation_behavior: string;
declare jurisdiction_country_code: string;
declare jurisdiction_region_code: CreationOptional<string | null>;
declare tax_scope: string;
declare invoice_note: CreationOptional<string | null>;
declare allowed_surcharge_codes: CreationOptional<object | null>;
declare is_system: boolean;
declare is_active: boolean;
declare valid_from: CreationOptional<string | null>;
declare valid_to: CreationOptional<string | null>;
static associate(_database: Sequelize) {}
static hooks(_database: Sequelize) {}
}
export default (database: Sequelize) => {
TaxDefinitionModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
company_id: {
type: DataTypes.UUID,
allowNull: false,
},
code: {
type: DataTypes.STRING(40),
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
},
rate_value: {
type: DataTypes.SMALLINT,
allowNull: false,
},
rate_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 2,
},
tax_family: {
type: DataTypes.STRING(40),
allowNull: false,
},
calculation_behavior: {
type: DataTypes.STRING(40),
allowNull: false,
},
jurisdiction_country_code: {
type: DataTypes.STRING(8),
allowNull: false,
},
jurisdiction_region_code: {
type: DataTypes.STRING(16),
allowNull: true,
defaultValue: null,
},
tax_scope: {
type: DataTypes.STRING(40),
allowNull: false,
},
invoice_note: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
},
allowed_surcharge_codes: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: null,
},
is_system: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
valid_from: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
valid_to: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
},
{
sequelize: database,
modelName: "TaxDefinitionModel",
tableName: "tax_definitions",
underscored: true,
paranoid: true,
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{
name: "idx_tax_definitions_company_code",
fields: ["company_id", "code", "deleted_at"],
unique: true,
},
],
whereMergeStrategy: "and",
defaultScope: {},
scopes: {},
}
);
return TaxDefinitionModel;
};

View File

@ -0,0 +1 @@
export * from "./sequelize-tax-definition.repository";

View File

@ -0,0 +1,174 @@
import {
EntityNotFoundError,
InfrastructureRepositoryError,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import type { ITaxDefinitionRepository, TaxDefinitionSummary } from "../../../../../application";
import type { TaxDefinition, TaxDefinitionCode } from "../../../../../domain";
import type {
SequelizeTaxDefinitionDomainMapper,
SequelizeTaxDefinitionSummaryMapper,
} from "../mappers";
import { TaxDefinitionModel } from "../models";
export class SequelizeTaxDefinitionRepository
extends SequelizeRepository<TaxDefinition>
implements ITaxDefinitionRepository
{
constructor(
private readonly domainMapper: SequelizeTaxDefinitionDomainMapper,
private readonly summaryMapper: SequelizeTaxDefinitionSummaryMapper,
database: Sequelize
) {
super({ database });
}
async create(
taxDefinition: TaxDefinition,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(taxDefinition);
if (dtoResult.isFailure) return Result.fail(dtoResult.error);
await TaxDefinitionModel.create(dtoResult.data, { transaction });
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async update(
taxDefinition: TaxDefinition,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(taxDefinition);
if (dtoResult.isFailure) return Result.fail(dtoResult.error);
const { id, ...payload } = dtoResult.data as any;
const [affected] = await TaxDefinitionModel.update(payload, {
where: { id },
transaction,
individualHooks: true,
});
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or tax definition not found")
);
}
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await TaxDefinitionModel.count({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok(Boolean(count > 0));
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<TaxDefinition, Error>> {
try {
const row = await TaxDefinitionModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (!row) return Result.fail(new EntityNotFoundError("TaxDefinition", "id", id.toString()));
return this.domainMapper.mapToDomain(row);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async getByCodeInCompany(
companyId: UniqueID,
code: TaxDefinitionCode,
transaction?: Transaction
): Promise<Result<TaxDefinition, Error>> {
try {
const row = await TaxDefinitionModel.findOne({
where: { code: code.toString(), company_id: companyId.toString() },
transaction,
});
if (!row)
return Result.fail(new EntityNotFoundError("TaxDefinition", "code", code.toString()));
return this.domainMapper.mapToDomain(row);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<TaxDefinitionSummary>, Error>> {
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
mappings: {
isActive: "is_active",
},
searchableFields: ["code", "name", "description", "invoice_note"],
sortableFields: ["code", "name"],
enableFullText: true,
database: this.database,
strictMode: true,
});
query.where = { ...query.where, company_id: companyId.toString(), deleted_at: null };
const [rows, count] = await Promise.all([
TaxDefinitionModel.findAll({ ...query, transaction }),
TaxDefinitionModel.count({ where: query.where, distinct: true, transaction }),
]);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const deleted = await TaxDefinitionModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (deleted === 0)
return Result.fail(new EntityNotFoundError("TaxDefinition", "id", id.toString()));
return Result.ok(true);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
}

View File

@ -4,3 +4,4 @@ export {
} from "./manifest";
export * from "./payment-methods";
export * from "./payment-terms";
export * from "./tax-regimes";

View File

@ -8,7 +8,7 @@ export const CatalogsModuleManifest: IModuleClient = {
version: MODULE_VERSION,
dependencies: ["auth", "Core"],
protected: true,
layout: "app",
layout: "app-sidebar",
routes: (params: ModuleClientParams) => {
return [];

View File

@ -0,0 +1,2 @@
export * from "./shared";
export * from "./utils";

View File

@ -0,0 +1 @@
export * from "./list-tax-regimes.adapter";

View File

@ -0,0 +1,54 @@
import type { ListTaxRegimesResponseDTO } from "../../../../common";
import type { ListTaxRegimesResult } from "../api";
import type { TaxRegimeList, TaxRegimeListRow } from "../entities";
/**
* Adaptador para transformar los datos de la API de ListTaxRegimesResult
* a la entidad TaxRegimeList utilizada en la aplicación.
* Reglas de adaptación:
* - page, per_page, total_pages, total_items se asignan directamente.
* - items se transforma utilizando TaxRegimeListRowAdapter para cada elemento.
*
* @param pageDto - lista de proformas desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {TaxRegimeList} Objeto adaptado a PaymentMehodList.
*/
export const ListTaxRegimesAdapter = {
fromDto(dto: ListTaxRegimesResult, context?: unknown): TaxRegimeList {
return {
page: dto.page,
perPage: dto.per_page,
totalPages: dto.total_pages,
totalItems: dto.total_items,
items: dto.items.map((rowDto) => TaxRegimeListRowAdapter.fromDto(rowDto, context)),
};
},
};
/**
* Adaptador para transformar los items de la API de ListPaymentMehodsResult a la entidad PaymentMehodListRow.
* Reglas de adaptación:
* - id, company_id se asignan directamente.
*
* @param rowDto - item de proforma desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {TaxRegimeListRow} Objeto adaptado a PaymentMehodListRow.
*/
type ListTaxRegimesItemOutput = ListTaxRegimesResponseDTO["items"][number];
const TaxRegimeListRowAdapter = {
fromDto(dto: ListTaxRegimesItemOutput, context?: unknown): TaxRegimeListRow {
return {
id: dto.id,
companyId: dto.company_id,
code: dto.code,
description: dto.description,
isSystem: dto.is_system,
isActive: dto.is_active,
};
},
};

View File

@ -0,0 +1 @@
export * from "./list-tax-regimes-by-criteria.api";

View File

@ -0,0 +1,38 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListTaxRegimesResponseDTO } from "../../../../common";
/**
* Recupera una lista de regímenes fiscales del sistema utilizando la
* fuente de datos proporcionada y los criterios de búsqueda especificados.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para listar los regímenes fiscales, incluyendo los criterios de búsqueda.
* @returns Una promesa que resuelve con una lista de regímenes fiscales que cumplen con los criterios especificados.
* @throws Error si la recuperación de la lista de regímenes fiscales falla.
*/
export type ListTaxRegimesByCriteriaParams = {
criteria?: CriteriaDTO;
signal?: AbortSignal;
};
export type ListTaxRegimesResult = ListTaxRegimesResponseDTO;
export function getListTaxRegimesByCriteria(
dataSource: IDataSource,
params: ListTaxRegimesByCriteriaParams
): Promise<ListTaxRegimesResult> {
const { criteria, signal } = params || {
criteria: {
page: 1,
per_page: 9999,
},
signal: undefined,
};
return dataSource.getList<ListTaxRegimesResponseDTO>("catalogs/tax-regimes", {
signal,
...criteria,
});
}

View File

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

View File

@ -0,0 +1,17 @@
/**
* Interface que representa una fila de la lista de
* regímenes fiscales en el sistema, adaptada desde la respuesta de la API.
* Contiene los campos justos para mostrar
* la información básica de cada régimen fiscal en la lista.
*/
export interface TaxRegimeListRow {
id: string;
companyId: string;
code: string;
description: string;
isSystem: boolean;
isActive: boolean;
}

View File

@ -0,0 +1,14 @@
import type { TaxRegimeListRow } from "./tax-regime-list-row.entity";
/**
* Interface que representa la respuesta paginada de una lista de regímenes fiscales,
* adaptada desde la respuesta de la API.
*/
export interface TaxRegimeList {
items: TaxRegimeListRow[];
totalPages: number;
totalItems: number;
page: number;
perPage: number;
}

View File

@ -0,0 +1,10 @@
export interface TaxRegime {
id: string;
companyId: string;
code: string;
description: string;
isSystem: boolean;
isActive: boolean;
}

View File

@ -0,0 +1 @@
export * from "./use-tax-regime-list-query";

View File

@ -0,0 +1,44 @@
import type { QueryKey } from "@tanstack/react-query";
import type { ListTaxRegimesRequestDTO } from "../../../../common";
/**
* Prefijo base para listados
*/
export const LIST_TAX_REGIMES_QUERY_KEY_PREFIX = ["tax-regimes"] as const;
/**
* Query key para listado de tax regimes
*/
export const LIST_TAX_REGIMES_QUERY_KEY = (criteria?: ListTaxRegimesRequestDTO): QueryKey =>
[
...LIST_TAX_REGIMES_QUERY_KEY_PREFIX,
{
pageNumber: criteria?.pageNumber ?? 1,
pageSize: criteria?.pageSize ?? 5,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
] as const;
/**
* Query key para detalle de tax regime
*/
export const TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX = ["tax-regimes:detail"] as const;
export const TAX_REGIME_QUERY_KEY = (taxRegimeId?: string): QueryKey => [
...TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX,
{ taxRegimeId },
];
/**
* Keys para mutaciones
*/
export const CREATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:create"] as const;
export const UPDATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:update"] as const;
export const DELETE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:delete"] as const;
/**
* Operaciones de dominio
*/

View File

@ -0,0 +1,32 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { ListTaxRegimesAdapter } from "../adapters";
import { getListTaxRegimesByCriteria } from "../api";
import type { TaxRegimeList } from "../entities";
import { LIST_TAX_REGIMES_QUERY_KEY } from "./keys";
export interface TaxRegimesListQueryOptions {
enabled?: boolean;
criteria?: Partial<CriteriaDTO>;
}
export const useTaxRegimesListQuery = (
options?: TaxRegimesListQueryOptions
): UseQueryResult<TaxRegimeList, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<TaxRegimeList, DefaultError>({
queryKey: LIST_TAX_REGIMES_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
const dto = await getListTaxRegimesByCriteria(dataSource, { signal, criteria });
return ListTaxRegimesAdapter.fromDto(dto);
},
enabled,
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
});
};

View File

@ -0,0 +1,2 @@
export * from "./entities";
export * from "./hooks";

View File

@ -0,0 +1 @@
export * from "./tax-regime-options.utils";

View File

@ -0,0 +1,10 @@
import type { SelectFieldItem } from "@repo/rdx-ui/components";
import type { TaxRegimeListRow } from "../shared";
export const getTaxRegimeOptions = (taxRegimes: TaxRegimeListRow[]): SelectFieldItem[] => {
return taxRegimes.map((taxRegime) => ({
value: taxRegime.id,
label: taxRegime.description,
}));
};

View File

@ -180,6 +180,14 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
);
});
toPatchField(dto.tax_regime_code).ifSet((taxRegimeCode) => {
proformaPatchProps.taxRegimeCode = extractOrPushError(
maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))),
"tax_regime_code",
errors
);
});
if (dto.items !== undefined) {
proformaPatchProps.items = this.mapItemsProps(dto.items, { errors });
}

View File

@ -53,6 +53,7 @@ export interface IProformaCreateProps {
linkedInvoiceId: Maybe<UniqueID>;
paymentMethodId: Maybe<UniqueID>;
taxRegimeCode: Maybe<string>;
items: IProformaItemCreateProps[];
globalDiscountPercentage: DiscountPercentage;
@ -100,6 +101,7 @@ export interface IProforma {
currencyCode: CurrencyCode;
paymentMethodId: Maybe<UniqueID>;
taxRegimeCode: Maybe<string>;
linkedInvoiceId: Maybe<UniqueID>;
@ -262,6 +264,10 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.props.paymentMethodId;
}
public get taxRegimeCode(): Maybe<string> {
return this.props.taxRegimeCode;
}
public get linkedInvoiceId(): Maybe<UniqueID> {
return this.props.linkedInvoiceId;
}
@ -290,6 +296,10 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.paymentMethodId.isSome();
}
public get hasTaxRegime() {
return this.taxRegimeCode.isSome();
}
public issue(): Result<void, Error> {
// Antes de cambiar el estado de la proforma,
// comprobamos que se cumplen las condiciones

View File

@ -64,6 +64,10 @@ export class CustomerInvoiceModel extends Model<
declare payment_method_id: CreationOptional<string | null>;
declare payment_method_description: CreationOptional<string | null>;
// Tax regime
declare tax_regime_code: CreationOptional<string | null>;
declare tax_regime_description: CreationOptional<string | null>;
// Subtotal
declare subtotal_amount_value: number;
declare subtotal_amount_scale: number;
@ -310,6 +314,16 @@ export default (database: Sequelize) => {
defaultValue: null,
},
tax_regime_code: {
type: DataTypes.STRING(),
allowNull: true,
},
tax_regime_description: {
type: new DataTypes.STRING(),
allowNull: true,
},
subtotal_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false,

View File

@ -1,4 +1,5 @@
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import type { ICatalogPublicServices } from "@erp/catalogs/api";
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
import {
type ChangeStatusProformaUseCase,
@ -50,11 +51,11 @@ export type ProformasInternalDeps = {
};
export function buildProformasDependencies(params: ModuleParams): ProformasInternalDeps {
const { database } = params;
const { database, getService } = params;
const catalogs = getService<ICatalogPublicServices>("catalogs");
// Infrastructure
const transactionManager = buildTransactionManager(database);
const catalogs = buildCatalogs();
const persistenceMappers = buildProformaPersistenceMappers(catalogs);
const repository = buildProformaRepository({ database, mappers: persistenceMappers });

View File

@ -1,260 +0,0 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
import {
type IProformaItemCreateProps,
InvoiceNumber,
InvoicePaymentMethod,
type InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
IssuedInvoiceItem,
type IssuedInvoiceItemProps,
ItemAmount,
ItemDescription,
ItemQuantity,
type ProformaCreateProps,
} from "../../../../domain";
/**
* CreateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
export class CreateProformaRequestMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
private errors: ValidationErrorDetail[] = [];
private languageCode?: LanguageCode;
private currencyCode?: CurrencyCode;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
this.errors = [];
}
public map(dto: CreateProformaRequestDTO, params: { companyId: UniqueID }) {
const { companyId } = params;
try {
this.errors = [];
const defaultStatus = InvoiceStatus.draft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
const customerId = extractOrPushError(
UniqueID.create(dto.customer_id),
"customer_id",
this.errors
);
const recipient = Maybe.none<InvoiceRecipient>();
const proformaNumber = extractOrPushError(
InvoiceNumber.create(dto.invoice_number),
"invoice_number",
this.errors
);
const series = extractOrPushError(
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
"series",
this.errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date),
"invoice_date",
this.errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
this.errors
);
const reference = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"reference",
this.errors
);
const description = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"description",
this.errors
);
const notes = extractOrPushError(
maybeFromNullableResult(dto.notes, (value) => TextValue.create(value)),
"notes",
this.errors
);
this.languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
this.errors
);
this.currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
this.errors
);
const paymentMethod = extractOrPushError(
maybeFromNullableResult(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
this.errors
);
const globalDiscountPercentage = extractOrPushError(
Percentage.create({
value: Number(dto.global_discount_percentage.value),
scale: Number(dto.global_discount_percentage.scale),
}),
"discount_percentage",
this.errors
);
const items = this.mapItems(dto.items);
if (this.errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", this.errors)
);
}
const proformaProps: Omit<ProformaCreateProps, "items"> & { items: IProformaItemCreateProps[] } = {
companyId,
status: defaultStatus!,
invoiceNumber: proformaNumber!,
series: series!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
customerId: customerId!,
recipient: recipient!,
reference: reference!,
description: description!,
notes: notes!,
languageCode: this.languageCode!,
currencyCode: this.currencyCode!,
paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!,
items:
};
return Result.ok({ id: proformaId!, props: proformaProps });
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemCreateProps[] {
const proformaItems = CustomerInvoiceItems.create({
currencyCode: this.currencyCode!,
languageCode: this.languageCode!,
items: [],
});
items.forEach((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
"description",
this.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (value) => ItemQuantity.create(value)),
"quantity",
this.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (value) => ItemAmount.create(value)),
"unit_amount",
this.errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create(value)
),
"discount_percentage",
this.errors
);
const taxes = this.mapTaxes(item, index);
const itemProps: IssuedInvoiceItemProps = {
currencyCode: this.currencyCode!,
languageCode: this.languageCode!,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!,
taxes: taxes,
};
const itemResult = IssuedInvoiceItem.create(itemProps);
if (itemResult.isSuccess) {
proformaItems.add(itemResult.data);
} else {
this.errors.push({
path: `items[${index}]`,
message: itemResult.error.message,
});
}
});
return proformaItems;
}
private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) {
const taxes = ItemTaxes.create([]);
item.taxes.split(",").forEach((tax_code, taxIndex) => {
const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);
if (taxResult.isSuccess) {
taxes.add(taxResult.data);
} else {
this.errors.push({
path: `items[${itemIndex}].taxes[${taxIndex}]`,
message: taxResult.error.message,
});
}
});
return taxes;
}
}

View File

@ -1,4 +1,5 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import type { ICatalogPublicServices } from "@erp/catalogs/api";
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
@ -34,9 +35,11 @@ export const proformasRouter = (params: StartParams) => {
const deps = getInternal<ProformasInternalDeps>("customer-invoices", "proformas");
const issuedInvoicesServices = getService<IIssuedInvoicePublicServices>("self:issuedInvoices");
const catalogServices = getService<ICatalogPublicServices>("self:catalogs");
const publicServices = {
issuedInvoiceServices: issuedInvoicesServices,
catalogServices: catalogServices,
};
const router: Router = Router({ mergeParams: true });

View File

@ -1,4 +1,4 @@
import type { JsonPaymentCatalogProvider } from "@erp/core";
import type { IPaymentMethodPublicServices, ITaxRegimePublicServices } from "@erp/catalogs/api";
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
@ -40,18 +40,21 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
private _paymentCatalog: JsonPaymentCatalogProvider;
private _paymentMethodCatalog: IPaymentMethodPublicServices;
private _taxRegimeCatalog: ITaxRegimePublicServices;
constructor(params: MapperParamsType) {
super();
const { paymentCatalog } = params as {
paymentCatalog: JsonPaymentCatalogProvider;
const { paymentCatalog, taxRegimeCatalog } = params as {
paymentCatalog: IPaymentMethodPublicServices;
taxRegimeCatalog: ITaxRegimePublicServices;
};
this._paymentCatalog = paymentCatalog;
this._paymentMethodCatalog = paymentCatalog;
this._taxRegimeCatalog = taxRegimeCatalog;
if (!this._paymentCatalog) {
if (!this._paymentMethodCatalog) {
throw new Error('paymentCatalog not defined ("SequelizeProformaDomainMapper")');
}
@ -168,6 +171,13 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
errors
);
// Tax regime code
const taxRegimeCode = extractOrPushError(
maybeFromNullableResult(raw.tax_regime_code, (value) => Result.ok(String(value))),
"tax_regime_code",
errors
);
// % descuento global (VO)
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({
@ -198,6 +208,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
languageCode,
currencyCode,
paymentMethodId,
taxRegimeCode,
globalDiscountPercentage,
linkedInvoiceId,
@ -267,6 +278,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
paymentMethodId: attributes.paymentMethodId!,
taxRegimeCode: attributes.taxRegimeCode!,
linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia.
};
@ -328,7 +341,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
if (source.hasPaymentMethod) {
const paymentId = source.paymentMethodId.unwrap();
const paymentOrNot = this._paymentCatalog.findById(paymentId.toString());
const paymentOrNot = this._paymentMethodCatalog.findById(paymentId.toString());
if (paymentOrNot.isSome()) {
const paymentItem = paymentOrNot.unwrap();
@ -340,7 +353,27 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
}
}
// 5) Si hubo errores de mapeo, devolvemos colección de validación
// 5) Tax regime
let taxRegime: {
code: string | null;
description: string | null;
} = { code: null, description: null };
if (source.hasTaxRegime) {
const taxRegimeCode = source.taxRegimeCode.unwrap();
const taxRegimeOrNot = this._taxRegimeCatalog.findBy(taxRegimeCode.toString());
if (taxRegimeOrNot.isSome()) {
const taxRegimeItem = taxRegimeOrNot.unwrap();
taxRegime = {
code: taxRegimeItem.code ?? null,
description: taxRegimeItem.description ?? null,
};
}
}
// 6) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
@ -376,6 +409,9 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
payment_method_id: payment.id,
payment_method_description: payment.description,
tax_regime_code: taxRegime.code,
tax_regime_description: taxRegime.description,
subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale,

View File

@ -53,6 +53,7 @@ export const UpdateProformaByIdRequestSchema = z.object({
payment_method_id: z.uuid().nullable().optional(),
payment_term_id: z.uuid().nullable().optional(),
tax_regime_code: z.string().nullable().optional(),
// retención como código??? retencion_15

View File

@ -13,6 +13,7 @@ import {
IssuedInvoiceRecipientSummarySchema,
IssuedInvoiceStatusSchema,
PaymentMethodRefSchema,
TaxRegimeRefSchema,
TaxesBreakdownSchema,
VerifactuRecordSchema,
} from "../../shared";
@ -45,6 +46,8 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
payment_method: PaymentMethodRefSchema.nullable(),
tax_regime: TaxRegimeRefSchema.nullable(),
subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema,

View File

@ -8,7 +8,12 @@ import {
} from "@erp/core";
import { z } from "zod/v4";
import { PaymentMethodRefSchema, PaymentTermRefSchema, TaxesBreakdownSchema } from "../../shared";
import {
PaymentMethodRefSchema,
PaymentTermRefSchema,
TaxRegimeRefSchema,
TaxesBreakdownSchema,
} from "../../shared";
import {
ProformaItemDetailSchema,
ProformaRecipientSummarySchema,
@ -43,6 +48,8 @@ export const GetProformaByIdResponseSchema = z.object({
payment_method: PaymentMethodRefSchema.nullable(),
payment_term: PaymentTermRefSchema.nullable(),
tax_regime: TaxRegimeRefSchema.nullable(),
subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema,

View File

@ -4,4 +4,5 @@ export * from "./payment-method-ref.dto";
export * from "./payment-term-ref.dto";
export * from "./proforma";
export * from "./tax-combination-code.dto";
export * from "./tax-regime-ref.dto";
export * from "./taxes-breakdown.dto";

View File

@ -0,0 +1,8 @@
import { z } from "zod/v4";
export const TaxRegimeRefSchema = z.object({
code: z.string(),
description: z.string(),
});
export type TaxRegimeRefDTO = z.infer<typeof TaxRegimeRefSchema>;

View File

@ -46,6 +46,8 @@ export const GetProformaByIdAdapter = {
paymentMethodId: dto.payment_method?.id ?? null,
paymentTermId: dto.payment_term?.id ?? null,
taxRegimeCode: dto.tax_regime?.code ?? null,
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),

View File

@ -31,6 +31,7 @@ export interface Proforma {
customerId: string;
recipient: ProformaRecipient;
taxRegimeCode: string | null;
taxes: ProformaTaxSummary[];
paymentMethodId: string | null;

View File

@ -39,7 +39,7 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
proforma.globalDiscountPercentage ?? proformaDefaults.globalDiscountPercentage,
taxMode: fiscalDefaults.taxMode,
taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API
taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode,
hasTaxPercentage:
fiscalDefaults.taxMode === "single" && fiscalDefaults.defaultTaxPercentage !== null,

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef } from "react";
import { getTaxRegimeOptions, useTaxRegimesListQuery } from "@erp/catalogs/client/tax-regimes";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { type UseFormReturn, useWatch } from "react-hook-form";
import {
@ -6,34 +7,15 @@ import {
type ProformaTaxPercentageOption,
getProformaRecPercentage,
} from "../../shared";
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
import type { ProformaUpdateForm } from "../entities";
interface UseUpdateProformaTaxControllerParams {
form: UseFormReturn<ProformaUpdateForm>;
}
export interface UseUpdateProformaTaxControllerResult {
taxMode: ProformaTaxMode;
hasTaxPercentage: boolean;
taxPercentage: number | null;
hasRecPercentage: boolean;
recPercentage: number | null;
hasRetentionPercentage: boolean;
retentionPercentage: number | null;
usesSingleTax: boolean;
usesPerLineTax: boolean;
enablePerLineTaxes: () => void;
disablePerLineTaxes: () => void;
updateTaxPercentage: (newTaxPercentage: ProformaTaxPercentageOption) => void;
updateRecPercentage: (enabled: boolean) => void;
updateRetentionPercentage: (newRetentionPercentage: ProformaRetentionPercentageOption) => void;
}
export type UseUpdateProformaTaxControllerResult = ReturnType<
typeof useUpdateProformaTaxController
>;
const resolveRecPercentage = (
enabled: boolean,
@ -46,9 +28,7 @@ const resolveRecPercentage = (
return getProformaRecPercentage(taxPercentage as ProformaTaxPercentageOption);
};
export const useUpdateProformaTaxController = ({
form,
}: UseUpdateProformaTaxControllerParams): UseUpdateProformaTaxControllerResult => {
export const useUpdateProformaTaxController = ({ form }: UseUpdateProformaTaxControllerParams) => {
const { control, getValues, setValue } = form;
const taxMode = useWatch({ control, name: "taxMode" });
@ -64,6 +44,18 @@ export const useUpdateProformaTaxController = ({
const hasMountedRef = useRef(false);
const taxRegimesQuery = useTaxRegimesListQuery({
criteria: {
filters: [
{
field: "isActive",
operator: "EQUALS",
value: "true",
},
],
},
});
useEffect(() => {
if (taxMode !== "single") return;
@ -180,6 +172,10 @@ export const useUpdateProformaTaxController = ({
[setValue]
);
const taxRegimeOptions = useMemo(() => {
return getTaxRegimeOptions(taxRegimesQuery.data?.items ?? []);
}, [taxRegimesQuery.data?.items]);
return {
taxMode,
@ -201,5 +197,7 @@ export const useUpdateProformaTaxController = ({
updateTaxPercentage,
updateRecPercentage,
updateRetentionPercentage,
taxRegimeOptions,
};
};

View File

@ -15,7 +15,6 @@
export interface ProformaItemUpdatePatch {
id: string;
position: number;
isValued: boolean;
description: string | null;

View File

@ -1,7 +1,6 @@
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
import type { CustomerSelectionOption } from "@erp/customers";
import { PercentageField } from "@repo/rdx-ui/components";
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
import { cn } from "@repo/shadcn-ui/lib/utils";
@ -14,7 +13,6 @@ import type {
UseUpdateProformaTaxControllerResult,
UseUpdateProformaTotalsControllerResult,
} from "../../controllers";
import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary";
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
@ -89,24 +87,10 @@ export const ProformaUpdateEditorForm = ({
selectedCustomer={selectedCustomer}
/>
<ProformaUpdateTaxEditor className="2xl:col-span-1" taxCtrl={taxCtrl} />
<NewProformaTotalsSummary
className="hidden"
currency={currencyCode}
globalDiscountField={
<PercentageField
className="md:col-span-4 md:col-start-1"
disabled={isSubmitting}
inputClassName="bg-background"
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
name="globalDiscountPercentage"
/>
}
layout="vertical"
showRec={taxCtrl.hasRecPercentage}
showRetention={taxCtrl.hasRetentionPercentage}
totals={totalsCtrl.totals}
<ProformaUpdateTaxEditor
className="2xl:col-span-1"
taxCtrl={taxCtrl}
taxRegimeOptions={taxCtrl.taxRegimeOptions}
/>
<ProformaUpdatePaymentEditor

View File

@ -2,6 +2,7 @@ import {
FormSectionCard,
FormSectionGrid,
SelectField,
type SelectFieldItem,
SwitchField,
} from "@repo/rdx-ui/components";
import { PercentageHelper } from "@repo/rdx-utils";
@ -19,6 +20,7 @@ import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
interface ProformaUpdateTaxEditorProps {
taxCtrl: UseUpdateProformaTaxControllerResult;
taxRegimeOptions: SelectFieldItem[];
disabled?: boolean;
readOnly?: boolean;
@ -28,6 +30,7 @@ interface ProformaUpdateTaxEditorProps {
export const ProformaUpdateTaxEditor = ({
taxCtrl,
taxRegimeOptions,
disabled = false,
readOnly = false,
className,
@ -50,60 +53,7 @@ export const ProformaUpdateTaxEditor = ({
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
items={[
{ value: "01", label: "01: Operación de régimen general." },
{ value: "02", label: "02: Exportación." },
{
value: "03",
label:
"03: Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.",
},
{ value: "04", label: "04: Régimen especial del oro de inversión." },
{ value: "05", label: "05: Régimen especial de las agencias de viajes." },
{
value: "06",
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
},
{ value: "07", label: "07: Régimen especial del criterio de caja." },
{ value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
{
value: "09",
label:
"09: Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)",
},
{
value: "10",
label:
"10: Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro.",
},
{ value: "11", label: "11: Operaciones de arrendamiento de local de negocio." },
{
value: "14",
label:
"14: Factura con IVA o IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.",
},
{
value: "15",
label:
"15: Factura con IVA o IGIC pendiente de devengo en operaciones de tracto sucesivo.",
},
{
value: "17",
label:
"17: Operación acogida a alguno de los regímenes previstos en el Capítulo XI del Título IX (OSS e IOSS) o régimen especial de comerciante minorista",
},
{
value: "18",
label:
"18: Recargo de equivalencia o régimen especial del pequeño empresario o profesional.",
},
{
value: "19",
label:
"19: Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP) u operaciones interiores exentas por aplicación artículo 25 Ley 19/1994",
},
{ value: "20", label: "20: Régimen simplificado" },
]}
items={taxRegimeOptions}
label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")}
name="taxRegimeCode"
placeholder={t(

View File

@ -38,6 +38,8 @@ export const buildUpdateProformaByIdParams = (
throw new Error("proformaId is required");
}
console.log(patch);
const data: UpdateProformaByIdParams["data"] = {};
if (ObjectHelper.hasOwn(patch, "series")) {
@ -84,6 +86,10 @@ export const buildUpdateProformaByIdParams = (
data.payment_term_id = patch.paymentTermId;
}
if (ObjectHelper.hasOwn(patch, "taxRegimeCode")) {
data.tax_regime_code = patch.taxRegimeCode;
}
if (ObjectHelper.hasOwn(patch, "globalDiscountPercentage")) {
data.global_discount_percentage = PercentageDTOHelper.fromNumber(
patch.globalDiscountPercentage!,