.
This commit is contained in:
parent
dc6382dd41
commit
5bc9920538
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
File diff suppressed because one or more lines are too long
@ -208,7 +208,7 @@ function validateModuleServices(moduleName: string, services: Record<string, unk
|
||||
const fullName = `${moduleName}:${serviceKey}`;
|
||||
|
||||
if (serviceApi === undefined) {
|
||||
throw new Error(`Service "${fullName}" is undefined`);
|
||||
throw new Error(`Service "${fullName}" is undefined (validateModuleServices)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ const services: Record<string, unknown> = {};
|
||||
* Registra un objeto de servicio (API) bajo un nombre.
|
||||
*/
|
||||
export function registerService(name: string, api: unknown) {
|
||||
console.debug(`Registering service: ${name}`);
|
||||
if (services[name]) {
|
||||
throw new Error(`❌ Servicio "${name}" ya fue registrado.`);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
export * from "./services";
|
||||
export * from "./tax-definitions";
|
||||
export * from "./tax-regimes";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export * from "./di";
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./public";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from './payment-method-not-active.error';
|
||||
@ -0,0 +1,9 @@
|
||||
import { DomainError, UniqueID } from "@repo/rdx-ddd";
|
||||
|
||||
export class PaymentMethodNotActiveError extends DomainError {
|
||||
public readonly code = "PAYMENT_METHOD_NOT_ACTIVE" as const;
|
||||
|
||||
public constructor(public readonly id: UniqueID) {
|
||||
super(`Payment method with id ${id.toString()} is not active`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./mappers";
|
||||
export * from "./models/";
|
||||
export * from "./services";
|
||||
export * from "./services/";
|
||||
@ -0,0 +1 @@
|
||||
export * from './payment-method-public-model.mapper';
|
||||
@ -0,0 +1,18 @@
|
||||
|
||||
import type {
|
||||
PaymentMethodPublicModel
|
||||
} from "../models";
|
||||
import { PaymentMethod } from '../../../../domain';
|
||||
|
||||
export class PaymentMethodPublicModelMapper {
|
||||
public toPublicModel(paymentMethod: PaymentMethod): PaymentMethodPublicModel {
|
||||
return {
|
||||
id: paymentMethod.id,
|
||||
companyId: paymentMethod.companyId,
|
||||
name: paymentMethod.name.toPrimitive(),
|
||||
description: paymentMethod.description.map((value) => value.toPrimitive()),
|
||||
isActive: paymentMethod.isActive,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './payment-method-public.model';
|
||||
@ -0,0 +1,14 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
export interface PaymentMethodPublicModel {
|
||||
id: UniqueID;
|
||||
companyId: UniqueID;
|
||||
|
||||
name: string;
|
||||
description: Maybe<string>;
|
||||
|
||||
isActive: boolean;
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './payment-method-public-finder.interface';
|
||||
export * from './payment-method-public-finder';
|
||||
@ -0,0 +1,24 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { PaymentMethodPublicModel } from "../models/payment-method-public.model";
|
||||
|
||||
export interface FindPaymentMethodByIdInCompanyParams {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
transaction?: unknown;
|
||||
}
|
||||
|
||||
export interface IPaymentMethodPublicFinder {
|
||||
existsByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
getByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<PaymentMethodPublicModel, Error>>;
|
||||
|
||||
findByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<Maybe<PaymentMethodPublicModel>, Error>>;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IPaymentMethodRepository } from "../../repositories";
|
||||
import type { PaymentMethodPublicModelMapper } from "../mappers/payment-method-public-model.mapper";
|
||||
import type { PaymentMethodPublicModel } from "../models/payment-method-public.model";
|
||||
import type {
|
||||
FindPaymentMethodByIdInCompanyParams,
|
||||
IPaymentMethodPublicFinder,
|
||||
} from "./payment-method-public-finder.interface";
|
||||
|
||||
export class PaymentMethodPublicFinder implements IPaymentMethodPublicFinder {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
repository: IPaymentMethodRepository;
|
||||
mapper: PaymentMethodPublicModelMapper;
|
||||
},
|
||||
) {}
|
||||
|
||||
public async existsByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<boolean, Error>> {
|
||||
const result = await this.deps.repository.existsByIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(result.data);
|
||||
}
|
||||
|
||||
public async getByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<PaymentMethodPublicModel, Error>> {
|
||||
const result = await this.deps.repository.getByIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(this.deps.mapper.toPublicModel(result.data));
|
||||
}
|
||||
|
||||
public async findByIdInCompany(
|
||||
params: FindPaymentMethodByIdInCompanyParams,
|
||||
): Promise<Result<Maybe<PaymentMethodPublicModel>, Error>> {
|
||||
const result = await this.deps.repository.findByIdInCompany(
|
||||
params.companyId,
|
||||
params.id,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
result.data.match(
|
||||
(paymentMethod) => Maybe.some(this.deps.mapper.toPublicModel(paymentMethod)),
|
||||
() => Maybe.none<PaymentMethodPublicModel>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
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 { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { PaymentMethod } from "../../../domain";
|
||||
import type { PaymentMethodSummary } from "../models";
|
||||
@ -18,6 +18,11 @@ export interface IPaymentMethodRepository {
|
||||
id: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<PaymentMethod, Error>>;
|
||||
findByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>>;
|
||||
findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
|
||||
@ -9,13 +9,13 @@ import type { IPaymentMethodRepository } from "../repositories";
|
||||
export interface IPaymentMethodFinder {
|
||||
findPaymentMethodById(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
paymentMethodId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<PaymentMethod, Error>>;
|
||||
|
||||
paymentmethodExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
paymentMethodId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
@ -31,18 +31,18 @@ export class PaymentMethodFinder implements IPaymentMethodFinder {
|
||||
|
||||
async findPaymentMethodById(
|
||||
companyId: UniqueID,
|
||||
paymentmethodId: UniqueID,
|
||||
paymentMethodId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<PaymentMethod, Error>> {
|
||||
return this.repository.getByIdInCompany(companyId, paymentmethodId, transaction);
|
||||
return this.repository.getByIdInCompany(companyId, paymentMethodId, transaction);
|
||||
}
|
||||
|
||||
async paymentmethodExists(
|
||||
companyId: UniqueID,
|
||||
paymentmethodId: UniqueID,
|
||||
paymentMethodId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, paymentmethodId, transaction);
|
||||
return this.repository.existsByIdInCompany(companyId, paymentMethodId, transaction);
|
||||
}
|
||||
|
||||
async findPaymentMethodsByCriteria(
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './catalog-public-services.interface';
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./public";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-definitions-not-found.error';
|
||||
@ -0,0 +1,10 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
import type { Collection } from "@repo/rdx-utils";
|
||||
|
||||
export class TaxDefinitionsNotFoundError extends DomainError {
|
||||
public readonly code = "TAX_DEFINITIONS_NOT_FOUND" as const;
|
||||
|
||||
public constructor(public readonly missingCodes: Collection<string>) {
|
||||
super(`Tax definitions not found for codes: ${missingCodes.getAll().join(", ")}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./errors/";
|
||||
export * from "./mappers";
|
||||
export * from "./models/";
|
||||
export * from "./services";
|
||||
export * from "./services/";
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-definition-public-model.mapper';
|
||||
@ -0,0 +1,80 @@
|
||||
import { Collection, Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import type { TaxDefinition } from "../../../../domain/tax-definitions";
|
||||
import type {
|
||||
TaxDefinitionPublicCalculationBehavior,
|
||||
TaxDefinitionPublicFamily,
|
||||
TaxDefinitionPublicModel,
|
||||
TaxDefinitionPublicScope,
|
||||
} from "../models/tax-definition-public.model";
|
||||
|
||||
export class TaxDefinitionPublicModelMapper {
|
||||
public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
|
||||
return {
|
||||
id: taxDefinition.id,
|
||||
companyId: Maybe.some(taxDefinition.companyId),
|
||||
code: taxDefinition.code.toPrimitive(),
|
||||
name: taxDefinition.name.toPrimitive(),
|
||||
description: taxDefinition.description.map((value) => value.toPrimitive()),
|
||||
rate: taxDefinition.rate,
|
||||
taxFamily: this.mapTaxFamily(taxDefinition.taxFamily.toPrimitive()),
|
||||
calculationBehavior: this.mapCalculationBehavior(
|
||||
taxDefinition.calculationBehavior.toPrimitive()
|
||||
),
|
||||
taxScope: this.mapTaxScope(taxDefinition.taxScope.toPrimitive()),
|
||||
jurisdictionCountryCode: taxDefinition.jurisdictionCountryCode,
|
||||
jurisdictionRegionCode: taxDefinition.jurisdictionRegionCode,
|
||||
invoiceNote: taxDefinition.invoiceNote.map((value) => value.toPrimitive()),
|
||||
allowedSurchargeCodes: new Collection(
|
||||
taxDefinition.allowedSurchargeCodes.match(
|
||||
(codes) => codes.map((code) => code.toPrimitive()),
|
||||
() => []
|
||||
)
|
||||
),
|
||||
isSystem: taxDefinition.isSystem,
|
||||
isActive: taxDefinition.isActive,
|
||||
validFrom: taxDefinition.validFrom,
|
||||
validTo: taxDefinition.validTo,
|
||||
};
|
||||
}
|
||||
|
||||
private mapTaxFamily(value: string): TaxDefinitionPublicFamily {
|
||||
switch (value) {
|
||||
case "iva":
|
||||
case "igic":
|
||||
case "ipsi":
|
||||
return value;
|
||||
case "equivalence_surcharge":
|
||||
return "surcharge";
|
||||
case "withholding":
|
||||
return "retention";
|
||||
default:
|
||||
throw new Error(`Unsupported tax family for public model: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
private mapCalculationBehavior(value: string): TaxDefinitionPublicCalculationBehavior {
|
||||
switch (value) {
|
||||
case "additive":
|
||||
case "subtractive":
|
||||
return value;
|
||||
default:
|
||||
throw new Error(`Unsupported calculation behavior for public model: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
private mapTaxScope(value: string): TaxDefinitionPublicScope {
|
||||
switch (value) {
|
||||
case "domestic":
|
||||
return "sales";
|
||||
case "intra_eu":
|
||||
case "import":
|
||||
return "purchases";
|
||||
case "export":
|
||||
case "international":
|
||||
return "both";
|
||||
default:
|
||||
throw new Error(`Unsupported tax scope for public model: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-definition-public.model';
|
||||
@ -0,0 +1,72 @@
|
||||
import type {
|
||||
CountryCode,
|
||||
CountryRegionCode, Percentage, UniqueID,
|
||||
UtcDate
|
||||
} from "@repo/rdx-ddd";
|
||||
import type { Collection, Maybe } from "@repo/rdx-utils";
|
||||
|
||||
/**
|
||||
* Familia fiscal pública expuesta por `catalogs`.
|
||||
*
|
||||
* No se expone el Value Object interno de dominio para evitar acoplar
|
||||
* consumidores externos a las invariantes privadas de `tax-definitions`.
|
||||
*/
|
||||
export type TaxDefinitionPublicFamily = "iva" | "igic" | "ipsi" | "surcharge" | "retention";
|
||||
|
||||
/**
|
||||
* Comportamiento de cálculo público de un impuesto.
|
||||
*
|
||||
* `additive` suma al total.
|
||||
* `subtractive` resta del total, por ejemplo retenciones.
|
||||
*/
|
||||
export type TaxDefinitionPublicCalculationBehavior = "additive" | "subtractive";
|
||||
|
||||
/**
|
||||
* Ámbito público de aplicación fiscal.
|
||||
*/
|
||||
export type TaxDefinitionPublicScope = "sales" | "purchases" | "both";
|
||||
|
||||
/**
|
||||
* Modelo público de lectura expuesto por el módulo `catalogs`.
|
||||
*
|
||||
* Es un contrato backend entre módulos, no un DTO HTTP y no una entidad
|
||||
* de dominio. Puede usar Value Objects comunes del ERP, pero no debe
|
||||
* exponer Value Objects internos de `tax-definitions`.
|
||||
*/
|
||||
export interface TaxDefinitionPublicModel {
|
||||
id: UniqueID;
|
||||
|
||||
/**
|
||||
* `none` si la definición es global/sistema.
|
||||
* `some(companyId)` si la definición está sobrescrita para una empresa.
|
||||
*/
|
||||
companyId: Maybe<UniqueID>;
|
||||
|
||||
code: string;
|
||||
name: string;
|
||||
description: Maybe<string>;
|
||||
|
||||
rate: Percentage;
|
||||
|
||||
taxFamily: TaxDefinitionPublicFamily;
|
||||
calculationBehavior: TaxDefinitionPublicCalculationBehavior;
|
||||
taxScope: TaxDefinitionPublicScope;
|
||||
|
||||
jurisdictionCountryCode: CountryCode;
|
||||
jurisdictionRegionCode: Maybe<CountryRegionCode>;
|
||||
|
||||
invoiceNote: Maybe<string>;
|
||||
|
||||
/**
|
||||
* Códigos de recargo compatibles con esta definición.
|
||||
*
|
||||
* Colección vacía => no permite recargos.
|
||||
*/
|
||||
allowedSurchargeCodes: Collection<string>;
|
||||
|
||||
isSystem: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
validFrom: Maybe<UtcDate>;
|
||||
validTo: Maybe<UtcDate>;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './tax-definition-public-finder.interface';
|
||||
export * from './tax-definition-public-finder';
|
||||
@ -0,0 +1,30 @@
|
||||
import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import type { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { TaxDefinitionPublicModel } from "../models/tax-definition-public.model";
|
||||
|
||||
export interface FindActiveTaxDefinitionByCodeParams {
|
||||
companyId: UniqueID;
|
||||
code: string;
|
||||
atDate: UtcDate;
|
||||
}
|
||||
|
||||
export interface FindActiveTaxDefinitionsByCodesParams {
|
||||
companyId: UniqueID;
|
||||
codes: Collection<string>;
|
||||
atDate: UtcDate;
|
||||
}
|
||||
|
||||
export interface ITaxDefinitionPublicFinder {
|
||||
findActiveByCode(
|
||||
params: FindActiveTaxDefinitionByCodeParams
|
||||
): Promise<Result<Maybe<TaxDefinitionPublicModel>, Error>>;
|
||||
|
||||
findActiveByCodes(
|
||||
params: FindActiveTaxDefinitionsByCodesParams
|
||||
): Promise<Result<Collection<TaxDefinitionPublicModel>, Error>>;
|
||||
|
||||
ensureActiveByCodes(
|
||||
params: FindActiveTaxDefinitionsByCodesParams
|
||||
): Promise<Result<Collection<TaxDefinitionPublicModel>, Error>>;
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { ITaxDefinitionRepository } from "../../repositories";
|
||||
import { TaxDefinitionsNotFoundError } from "../errors/tax-definitions-not-found.error";
|
||||
import type { TaxDefinitionPublicModelMapper } from "../mappers/tax-definition-public-model.mapper";
|
||||
import type {
|
||||
FindActiveTaxDefinitionByCodeParams,
|
||||
FindActiveTaxDefinitionsByCodesParams,
|
||||
ITaxDefinitionPublicFinder,
|
||||
} from "./tax-definition-public-finder.interface";
|
||||
import { TaxDefinitionPublicModel } from '../models';
|
||||
|
||||
export class TaxDefinitionPublicFinder implements ITaxDefinitionPublicFinder {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
repository: ITaxDefinitionRepository;
|
||||
mapper: TaxDefinitionPublicModelMapper;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async findActiveByCode(params: FindActiveTaxDefinitionByCodeParams): Promise<Result<Maybe<TaxDefinitionPublicModel>, Error>> {
|
||||
const result = await this.deps.repository.findActiveByCodeInCompany(params);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(result.data.map((taxDefinition) => this.deps.mapper.toPublicModel(taxDefinition)));
|
||||
}
|
||||
|
||||
public async findActiveByCodes(params: FindActiveTaxDefinitionsByCodesParams): Promise<Result<Collection<TaxDefinitionPublicModel>, Error>> {
|
||||
const result = await this.deps.repository.findActiveByCodesInCompany(params);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(new Collection(result.data.map((taxDefinition) => this.deps.mapper.toPublicModel(taxDefinition))));
|
||||
}
|
||||
|
||||
public async ensureActiveByCodes(params: FindActiveTaxDefinitionsByCodesParams):Promise<Result<Collection<TaxDefinitionPublicModel>, Error>> {
|
||||
const result = await this.findActiveByCodes(params);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const requestedCodes = Array.from(
|
||||
new Set(params.codes.getAll().map((code) => this.normalizeCode(code)))
|
||||
);
|
||||
const foundCodes = new Set(result.data.getAll().map((taxDefinition) => taxDefinition.code));
|
||||
const missingCodes = requestedCodes.filter((code) => !foundCodes.has(code));
|
||||
|
||||
if (missingCodes.length > 0) {
|
||||
return Result.fail(new TaxDefinitionsNotFoundError(new Collection(missingCodes)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private normalizeCode(code: string): string {
|
||||
return code.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
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 { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import type { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { TaxDefinition, TaxDefinitionCode } from "../../../domain";
|
||||
import type { TaxDefinitionSummary } from "../../tax-definitions/models";
|
||||
@ -32,6 +32,20 @@ export interface ITaxDefinitionRepository {
|
||||
transaction?: unknown
|
||||
): Promise<Result<TaxDefinition, Error>>;
|
||||
|
||||
findActiveByCodeInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
code: string;
|
||||
atDate: UtcDate;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<Maybe<TaxDefinition>, Error>>;
|
||||
|
||||
findActiveByCodesInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
codes: Collection<string>;
|
||||
atDate: UtcDate;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<Collection<TaxDefinition>, Error>>;
|
||||
|
||||
findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./tax-definition-creator.service";
|
||||
export * from "./tax-definition-deleter.service";
|
||||
export * from "./tax-definition-finder.service";
|
||||
export * from "./tax-definition-public-services";
|
||||
export * from "./tax-definition-status-changer.service";
|
||||
export * from "./tax-definition-updater.service";
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import type { ITaxDefinitionPublicFinder } from "../public";
|
||||
|
||||
export interface ITaxDefinitionPublicServices {
|
||||
finder: ITaxDefinitionPublicFinder;
|
||||
}
|
||||
|
||||
export const buildTaxDefinitionsPublicServices = (
|
||||
finder: ITaxDefinitionPublicFinder
|
||||
): ITaxDefinitionPublicServices => ({
|
||||
finder,
|
||||
});
|
||||
@ -1,4 +1,3 @@
|
||||
import type { TaxDefinitionDetailDTO } from "@erp/catalogs/common";
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
|
||||
import type { TaxDefinitionDetail } from "../../models";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./public";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-regime-not-found.error';
|
||||
@ -0,0 +1,10 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
import type { Collection } from "@repo/rdx-utils";
|
||||
|
||||
export class TaxRegimesNotFoundError extends DomainError {
|
||||
public readonly code = "TAX_REGIME_NOT_FOUND" as const;
|
||||
|
||||
public constructor(public readonly missingCodes: Collection<string>) {
|
||||
super(`Tax regime not found for codes: ${missingCodes.getAll().join(", ")}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
export { TaxRegimesNotFoundError } from "./errors/";
|
||||
export { TaxRegimePublicModelMapper } from "./mappers";
|
||||
export type {
|
||||
TaxRegimePublicCalculationBehavior,
|
||||
TaxRegimePublicFamily,
|
||||
TaxRegimePublicModel,
|
||||
TaxRegimePublicScope,
|
||||
} from "./models/";
|
||||
export type {
|
||||
FindActiveTaxRegimeByCodeParams,
|
||||
FindActiveTaxRegimesByCodesParams,
|
||||
ITaxRegimePublicFinder,
|
||||
} from "./services";
|
||||
export { TaxRegimePublicFinder } from "./services/";
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-regime-public-model.mapper';
|
||||
@ -0,0 +1,20 @@
|
||||
|
||||
import type { TaxRegime } from "../../../../domain/tax-regimes";
|
||||
import type {
|
||||
TaxRegimePublicModel
|
||||
} from "../models/tax-regime-public.model";
|
||||
|
||||
export class TaxRegimePublicModelMapper {
|
||||
public toPublicModel(TaxRegime: TaxRegime): TaxRegimePublicModel {
|
||||
return {
|
||||
id: TaxRegime.id,
|
||||
companyId: TaxRegime.companyId,
|
||||
code: TaxRegime.code.toPrimitive(),
|
||||
description: TaxRegime.description.toPrimitive(),
|
||||
isSystem: TaxRegime.isSystem,
|
||||
isActive: TaxRegime.isActive,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './tax-regime-public.model';
|
||||
@ -0,0 +1,20 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
|
||||
/**
|
||||
* Modelo público de lectura expuesto por el módulo `catalogs`.
|
||||
*
|
||||
* Es un contrato backend entre módulos, no un DTO HTTP y no una entidad
|
||||
* de dominio. Puede usar Value Objects comunes del ERP, pero no debe
|
||||
* exponer Value Objects internos de `tax-regimes`.
|
||||
*/
|
||||
export interface TaxRegimePublicModel {
|
||||
id: UniqueID;
|
||||
companyId: UniqueID;
|
||||
|
||||
code: string;
|
||||
description: string;
|
||||
|
||||
isSystem: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './tax-regime-public-finder.interface';
|
||||
export * from './tax-regime-public-finder';
|
||||
@ -0,0 +1,24 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { TaxRegimePublicModel } from "../models";
|
||||
|
||||
export interface FindTaxRegimeByCodeInCompanyParams {
|
||||
companyId: UniqueID;
|
||||
code: string;
|
||||
transaction?: unknown;
|
||||
}
|
||||
|
||||
export interface ITaxRegimePublicFinder {
|
||||
existsByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
getByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<TaxRegimePublicModel, Error>>;
|
||||
|
||||
findByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<Maybe<TaxRegimePublicModel>, Error>>;
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { TaxRegimeCode } from "../../../../domain/tax-regimes";
|
||||
import type { ITaxRegimeRepository } from "../../repositories";
|
||||
import type { TaxRegimePublicModelMapper } from "../mappers/tax-regime-public-model.mapper";
|
||||
import type { TaxRegimePublicModel } from "../models";
|
||||
import type {
|
||||
FindTaxRegimeByCodeInCompanyParams,
|
||||
ITaxRegimePublicFinder,
|
||||
} from "./tax-regime-public-finder.interface";
|
||||
|
||||
export class TaxRegimePublicFinder implements ITaxRegimePublicFinder {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
repository: ITaxRegimeRepository;
|
||||
mapper: TaxRegimePublicModelMapper;
|
||||
},
|
||||
) {}
|
||||
|
||||
public async existsByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<boolean, Error>> {
|
||||
const code = TaxRegimeCode.create(params.code);
|
||||
|
||||
if (code.isFailure) {
|
||||
return Result.fail(code.error);
|
||||
}
|
||||
|
||||
const result = await this.deps.repository.existsByCodeInCompany(
|
||||
params.companyId,
|
||||
code.data,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(result.data);
|
||||
}
|
||||
|
||||
public async getByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<TaxRegimePublicModel, Error>> {
|
||||
const code = TaxRegimeCode.create(params.code);
|
||||
|
||||
if (code.isFailure) {
|
||||
return Result.fail(code.error);
|
||||
}
|
||||
|
||||
const result = await this.deps.repository.getByCodeInCompany(
|
||||
params.companyId,
|
||||
code.data,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(this.deps.mapper.toPublicModel(result.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca un régimen fiscal por código dentro de una empresa.
|
||||
*
|
||||
* No falla si el régimen fiscal no existe. En ese caso devuelve `Maybe.none()`.
|
||||
* Usar `getByCodeInCompany` cuando la ausencia deba tratarse como error.
|
||||
*/
|
||||
public async findByCodeInCompany(
|
||||
params: FindTaxRegimeByCodeInCompanyParams,
|
||||
): Promise<Result<Maybe<TaxRegimePublicModel>, Error>> {
|
||||
const code = TaxRegimeCode.create(params.code);
|
||||
|
||||
if (code.isFailure) {
|
||||
return Result.fail(code.error);
|
||||
}
|
||||
|
||||
const result = await this.deps.repository.findByCodeInCompany(
|
||||
params.companyId,
|
||||
code.data,
|
||||
params.transaction,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
result.data.match(
|
||||
(taxRegime) => Maybe.some(this.deps.mapper.toPublicModel(taxRegime)),
|
||||
() => Maybe.none<TaxRegimePublicModel>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
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 { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { TaxRegime, TaxRegimeCode } from "../../../domain";
|
||||
import type { TaxRegimeSummary } from "../models";
|
||||
@ -20,6 +20,12 @@ export interface ITaxRegimeRepository {
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
existsByCodeInCompany(
|
||||
companyId: UniqueID,
|
||||
code: TaxRegimeCode,
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
getByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
@ -32,6 +38,12 @@ export interface ITaxRegimeRepository {
|
||||
transaction?: unknown
|
||||
): Promise<Result<TaxRegime, Error>>;
|
||||
|
||||
findByCodeInCompany(
|
||||
companyId: UniqueID,
|
||||
code: TaxRegimeCode,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Maybe<TaxRegime>, Error>>;
|
||||
|
||||
findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
|
||||
@ -61,24 +61,6 @@ export const isInvalidTaxDefinitionCalculationBehaviorError = (
|
||||
): 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;
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
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";
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,18 @@
|
||||
import { AggregateRoot, type TextValue, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
|
||||
import {
|
||||
AggregateRoot,
|
||||
type CountryCode,
|
||||
type CountryRegionCode,
|
||||
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";
|
||||
@ -23,8 +27,8 @@ export interface ITaxDefinitionCreateProps {
|
||||
rate: TaxRate;
|
||||
taxFamily: TaxFamily;
|
||||
calculationBehavior: TaxCalculationBehavior;
|
||||
jurisdictionCountryCode: TaxJurisdictionCountryCode;
|
||||
jurisdictionRegionCode: Maybe<TaxJurisdictionRegionCode>;
|
||||
jurisdictionCountryCode: CountryCode;
|
||||
jurisdictionRegionCode: Maybe<CountryRegionCode>;
|
||||
taxScope: TaxScope;
|
||||
invoiceNote: Maybe<TextValue>; // Texto fiscal que aparece en el documento
|
||||
allowedSurchargeCodes: Maybe<TaxDefinitionCode[]>;
|
||||
@ -146,19 +150,6 @@ export class TaxDefinition extends AggregateRoot<TaxDefinitionInternalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -198,11 +189,11 @@ export class TaxDefinition extends AggregateRoot<TaxDefinitionInternalProps> {
|
||||
return this.props.calculationBehavior;
|
||||
}
|
||||
|
||||
public get jurisdictionCountryCode(): TaxJurisdictionCountryCode {
|
||||
public get jurisdictionCountryCode(): CountryCode {
|
||||
return this.props.jurisdictionCountryCode;
|
||||
}
|
||||
|
||||
public get jurisdictionRegionCode(): Maybe<TaxJurisdictionRegionCode> {
|
||||
public get jurisdictionRegionCode(): Maybe<CountryRegionCode> {
|
||||
return this.props.jurisdictionRegionCode;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import {
|
||||
paymentMethodsRouter,
|
||||
paymentTermModels,
|
||||
paymentTermsRouter,
|
||||
taxDefinitionModels,
|
||||
taxDefinitionsRouter,
|
||||
taxRegimeModels,
|
||||
taxRegimesRouter,
|
||||
} from "./infrastructure";
|
||||
@ -13,10 +15,14 @@ import {
|
||||
buildCatalogsPublicServices,
|
||||
} from "./infrastructure/di/catalogs.di";
|
||||
|
||||
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 "./application/payment-methods/public";
|
||||
export * from "./application/tax-definitions/public";
|
||||
export * from "./application/tax-regimes/public";
|
||||
|
||||
//export * from "./infrastructure/payment-methods/persistence/sequelize"; <- ???
|
||||
|
||||
export type CatalogsPublicServicesType = ReturnType<typeof buildCatalogsPublicServices>;
|
||||
|
||||
export const catalogsAPIModule: IModuleServer = {
|
||||
name: "catalogs",
|
||||
version: "1.0.0",
|
||||
@ -44,13 +50,16 @@ export const catalogsAPIModule: IModuleServer = {
|
||||
|
||||
return {
|
||||
// Modelos Sequelize del módulo
|
||||
models: [...paymentMethodModels, ...paymentTermModels, ...taxRegimeModels],
|
||||
models: [
|
||||
...paymentMethodModels,
|
||||
...paymentTermModels,
|
||||
...taxDefinitionModels,
|
||||
...taxRegimeModels,
|
||||
],
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
paymentMethods: publicServices.paymentMethods,
|
||||
paymentTerms: publicServices.paymentTerms,
|
||||
taxRegimes: publicServices.taxRegimes,
|
||||
...publicServices,
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
@ -72,6 +81,7 @@ export const catalogsAPIModule: IModuleServer = {
|
||||
paymentMethodsRouter(params);
|
||||
paymentTermsRouter(params);
|
||||
taxRegimesRouter(params);
|
||||
taxDefinitionsRouter(params);
|
||||
|
||||
logger.info("🚀 Catalogs module started", {
|
||||
label: this.name,
|
||||
|
||||
@ -10,6 +10,11 @@ import {
|
||||
buildPaymentTermsDependencies,
|
||||
buildPaymentTermsPublicServices,
|
||||
} from "../payment-terms/di";
|
||||
import {
|
||||
type TaxDefinitionsInternalDeps,
|
||||
buildTaxDefinitionsDependencies,
|
||||
buildTaxDefinitionsPublicServices,
|
||||
} from "../tax-definitions/di";
|
||||
import {
|
||||
type TaxRegimesInternalDeps,
|
||||
buildTaxRegimesDependencies,
|
||||
@ -19,6 +24,7 @@ import {
|
||||
export type CatalogsInternalDeps = {
|
||||
paymentMethods: PaymentMethodsInternalDeps;
|
||||
paymentTerms: PaymentTermsInternalDeps;
|
||||
taxDefinitions: TaxDefinitionsInternalDeps;
|
||||
taxRegimes: TaxRegimesInternalDeps;
|
||||
};
|
||||
|
||||
@ -26,6 +32,7 @@ export const buildCatalogsDependencies = (params: ModuleParams): CatalogsInterna
|
||||
return {
|
||||
paymentMethods: buildPaymentMethodsDependencies(params),
|
||||
paymentTerms: buildPaymentTermsDependencies(params),
|
||||
taxDefinitions: buildTaxDefinitionsDependencies(params),
|
||||
taxRegimes: buildTaxRegimesDependencies(params),
|
||||
};
|
||||
};
|
||||
@ -34,6 +41,7 @@ export const buildCatalogsPublicServices = (params: SetupParams, deps: CatalogsI
|
||||
return {
|
||||
paymentMethods: buildPaymentMethodsPublicServices(params, deps.paymentMethods),
|
||||
paymentTerms: buildPaymentTermsPublicServices(params, deps.paymentTerms),
|
||||
taxDefinitions: buildTaxDefinitionsPublicServices(params, deps.taxDefinitions),
|
||||
taxRegimes: buildTaxRegimesPublicServices(params, deps.taxRegimes),
|
||||
};
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} 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 Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
import type { Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { IPaymentMethodRepository } from "../../../../../application";
|
||||
@ -145,6 +145,35 @@ export class SequelizePaymentMethodRepository
|
||||
}
|
||||
}
|
||||
|
||||
async findByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>> {
|
||||
try {
|
||||
const row = await PaymentMethodModel.findOne({
|
||||
where: {
|
||||
id: id.toString(),
|
||||
company_id: companyId.toString(),
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
return Result.ok(Maybe.none<PaymentMethod>());
|
||||
}
|
||||
|
||||
const mappedResult = this.domainMapper.mapToDomain(row);
|
||||
if (mappedResult.isFailure) {
|
||||
return Result.fail(mappedResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(mappedResult.data));
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||
*
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./tax-definition-persistence-mappers.di";
|
||||
export * from "./tax-definition-repositories.di";
|
||||
export * from "./tax-definitions.di";
|
||||
|
||||
@ -0,0 +1,125 @@
|
||||
import { type ModuleParams, type SetupParams, buildTransactionManager } from "@erp/core/api";
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
CreateTaxDefinitionInputMapper,
|
||||
CreateTaxDefinitionUseCase,
|
||||
DeleteTaxDefinitionByIdUseCase,
|
||||
DisableTaxDefinitionByIdUseCase,
|
||||
EnableTaxDefinitionByIdUseCase,
|
||||
GetTaxDefinitionByIdUseCase,
|
||||
type ITaxDefinitionPublicFinder,
|
||||
type ITaxDefinitionRepository,
|
||||
ListTaxDefinitionsUseCase,
|
||||
TaxDefinitionCreator,
|
||||
TaxDefinitionDeleter,
|
||||
TaxDefinitionFinder,
|
||||
TaxDefinitionFullSnapshotBuilder,
|
||||
TaxDefinitionPublicFinder,
|
||||
TaxDefinitionPublicModelMapper,
|
||||
TaxDefinitionStatusChanger,
|
||||
TaxDefinitionSummarySnapshotBuilder,
|
||||
TaxDefinitionUpdater,
|
||||
UpdateTaxDefinitionByIdInputMapper,
|
||||
UpdateTaxDefinitionByIdUseCase,
|
||||
} from "../../../application";
|
||||
|
||||
import { buildTaxDefinitionPersistenceMappers } from "./tax-definition-persistence-mappers.di";
|
||||
import { buildTaxDefinitionRepository } from "./tax-definition-repositories.di";
|
||||
|
||||
export type TaxDefinitionsInternalDeps = {
|
||||
repository: ITaxDefinitionRepository;
|
||||
useCases: {
|
||||
listTaxDefinitions: () => ListTaxDefinitionsUseCase;
|
||||
getTaxDefinitionById: () => GetTaxDefinitionByIdUseCase;
|
||||
createTaxDefinition: () => CreateTaxDefinitionUseCase;
|
||||
updateTaxDefinitionById: () => UpdateTaxDefinitionByIdUseCase;
|
||||
deleteTaxDefinitionById: () => DeleteTaxDefinitionByIdUseCase;
|
||||
disableTaxDefinitionById: () => DisableTaxDefinitionByIdUseCase;
|
||||
enableTaxDefinitionById: () => EnableTaxDefinitionByIdUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
export const buildTaxDefinitionsDependencies = (
|
||||
params: ModuleParams
|
||||
): TaxDefinitionsInternalDeps => {
|
||||
const { database } = params;
|
||||
|
||||
const transactionManager = buildTransactionManager(database as Sequelize);
|
||||
const persistenceMappers = buildTaxDefinitionPersistenceMappers();
|
||||
|
||||
const repository = buildTaxDefinitionRepository({ database, mappers: persistenceMappers });
|
||||
|
||||
const finder = new TaxDefinitionFinder(repository);
|
||||
const creator = new TaxDefinitionCreator(repository);
|
||||
const updater = new TaxDefinitionUpdater(repository);
|
||||
const deleter = new TaxDefinitionDeleter(repository);
|
||||
const statusChanger = new TaxDefinitionStatusChanger(repository);
|
||||
|
||||
const createInputMapper = new CreateTaxDefinitionInputMapper();
|
||||
const updateInputMapper = new UpdateTaxDefinitionByIdInputMapper();
|
||||
|
||||
const fullSnapshotBuilder = new TaxDefinitionFullSnapshotBuilder();
|
||||
const summarySnapshotBuilder = new TaxDefinitionSummarySnapshotBuilder();
|
||||
|
||||
return {
|
||||
repository,
|
||||
useCases: {
|
||||
listTaxDefinitions: () =>
|
||||
new ListTaxDefinitionsUseCase({
|
||||
repository,
|
||||
summarySnapshotBuilder,
|
||||
}),
|
||||
|
||||
getTaxDefinitionById: () =>
|
||||
new GetTaxDefinitionByIdUseCase({
|
||||
finder,
|
||||
fullSnapshotBuilder,
|
||||
}),
|
||||
|
||||
createTaxDefinition: () =>
|
||||
new CreateTaxDefinitionUseCase({
|
||||
dtoMapper: createInputMapper,
|
||||
creator,
|
||||
fullSnapshotBuilder,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
updateTaxDefinitionById: () =>
|
||||
new UpdateTaxDefinitionByIdUseCase({
|
||||
dtoMapper: updateInputMapper,
|
||||
updater,
|
||||
fullSnapshotBuilder,
|
||||
}),
|
||||
|
||||
deleteTaxDefinitionById: () =>
|
||||
new DeleteTaxDefinitionByIdUseCase({
|
||||
deleter,
|
||||
}),
|
||||
|
||||
disableTaxDefinitionById: () =>
|
||||
new DisableTaxDefinitionByIdUseCase({
|
||||
statusChanger,
|
||||
}),
|
||||
|
||||
enableTaxDefinitionById: () =>
|
||||
new EnableTaxDefinitionByIdUseCase({
|
||||
statusChanger,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildTaxDefinitionsPublicServices = (
|
||||
_params: SetupParams,
|
||||
deps: TaxDefinitionsInternalDeps
|
||||
): { finder: ITaxDefinitionPublicFinder } => {
|
||||
const mapper = new TaxDefinitionPublicModelMapper();
|
||||
|
||||
return {
|
||||
finder: new TaxDefinitionPublicFinder({
|
||||
repository: deps.repository,
|
||||
mapper,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import type { CreateTaxDefinitionRequestDTO } from "@erp/catalogs/common";
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { CreateTaxDefinitionUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class CreateTaxDefinitionController extends ExpressController {
|
||||
constructor(private readonly useCase: CreateTaxDefinitionUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const dto = this.req.body satisfies CreateTaxDefinitionRequestDTO;
|
||||
const result = await this.useCase.execute({ dto, companyId });
|
||||
|
||||
return result.match(
|
||||
(data: unknown) => this.created(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { DeleteTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class DeleteTaxDefinitionByIdController extends ExpressController {
|
||||
constructor(private readonly useCase: DeleteTaxDefinitionByIdUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { tax_definition_id } = this.req.params;
|
||||
const result = await this.useCase.execute(companyId, tax_definition_id as any);
|
||||
|
||||
return result.match(
|
||||
(data: unknown) => this.ok(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { DisableTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class DisableTaxDefinitionByIdController extends ExpressController {
|
||||
constructor(private readonly useCase: DisableTaxDefinitionByIdUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { tax_definition_id } = this.req.params;
|
||||
const result = await this.useCase.execute(companyId, tax_definition_id as any);
|
||||
|
||||
return result.match(
|
||||
(data: unknown) => this.ok(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { EnableTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class EnableTaxDefinitionByIdController extends ExpressController {
|
||||
constructor(private readonly useCase: EnableTaxDefinitionByIdUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { tax_definition_id } = this.req.params;
|
||||
const result = await this.useCase.execute(companyId, tax_definition_id as any);
|
||||
|
||||
return result.match(
|
||||
(data: unknown) => this.ok(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { GetTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class GetTaxDefinitionByIdController extends ExpressController {
|
||||
constructor(private readonly useCase: GetTaxDefinitionByIdUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { tax_definition_id } = this.req.params;
|
||||
const result = await this.useCase.execute(companyId, tax_definition_id as any);
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export * from "./create-tax-definition.controller";
|
||||
export * from "./delete-tax-definition-by-id.controller";
|
||||
export * from "./disable-tax-definition-by-id.controller";
|
||||
export * from "./enable-tax-definition-by-id.controller";
|
||||
export * from "./get-tax-definition-by-id.controller";
|
||||
export * from "./list-tax-definitions.controller";
|
||||
export * from "./update-tax-definition-by-id.controller";
|
||||
@ -0,0 +1,54 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
|
||||
import type { ListTaxDefinitionsUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class ListTaxDefinitionsController extends ExpressController {
|
||||
constructor(private readonly useCase: ListTaxDefinitionsUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
private getCriteriaWithDefaultOrder() {
|
||||
if (this.criteria.hasOrder()) {
|
||||
return this.criteria;
|
||||
}
|
||||
|
||||
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
|
||||
return Criteria.fromPrimitives(filters, "code", "ASC", pageSize, pageNumber, quicksearch);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const criteria = this.getCriteriaWithDefaultOrder();
|
||||
const result = await this.useCase.execute(companyId, criteria);
|
||||
|
||||
return result.match(
|
||||
(data: any) =>
|
||||
this.ok(data, {
|
||||
"X-Total-Count": String(data.total_items),
|
||||
"Pagination-Count": String(data.total_pages),
|
||||
"Pagination-Page": String(data.page),
|
||||
"Pagination-Limit": String(data.per_page),
|
||||
}),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import type { UpdateTaxDefinitionByIdRequestDTO } from "@erp/catalogs/common";
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { UpdateTaxDefinitionByIdUseCase } from "../../../../application/tax-definitions";
|
||||
import { taxDefinitionsApiErrorMapper } from "../tax-definitions-api-error-mapper";
|
||||
|
||||
export class UpdateTaxDefinitionByIdController extends ExpressController {
|
||||
constructor(private readonly useCase: UpdateTaxDefinitionByIdUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = taxDefinitionsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { tax_definition_id } = this.req.params;
|
||||
if (!tax_definition_id) {
|
||||
return this.invalidInputError("Tax definition ID missing");
|
||||
}
|
||||
|
||||
const dto = this.req.body as UpdateTaxDefinitionByIdRequestDTO;
|
||||
const result = await this.useCase.execute(companyId, { id: tax_definition_id as any, dto });
|
||||
|
||||
return result.match(
|
||||
(data: unknown) => this.ok(data),
|
||||
(err: Error) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./controllers";
|
||||
export * from "./tax-definitions.routes";
|
||||
@ -0,0 +1,186 @@
|
||||
import {
|
||||
ApiErrorMapper,
|
||||
ConflictApiError,
|
||||
EntityNotFoundError,
|
||||
type ErrorToApiRule,
|
||||
NotFoundApiError,
|
||||
ValidationApiError,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type InvalidTaxDefinitionAllowedSurchargeCodesError,
|
||||
type InvalidTaxDefinitionCalculationBehaviorError,
|
||||
type InvalidTaxDefinitionCodeError,
|
||||
type InvalidTaxDefinitionDescriptionError,
|
||||
type InvalidTaxDefinitionFamilyError,
|
||||
type InvalidTaxDefinitionIdError,
|
||||
type InvalidTaxDefinitionInvoiceNoteError,
|
||||
type InvalidTaxDefinitionNameError,
|
||||
type InvalidTaxDefinitionRateError,
|
||||
type InvalidTaxDefinitionScopeError,
|
||||
type InvalidTaxDefinitionValidityPeriodError,
|
||||
type TaxDefinitionCannotBeDisabledError,
|
||||
type TaxDefinitionCannotBeEnabledError,
|
||||
isInvalidTaxDefinitionAllowedSurchargeCodesError,
|
||||
isInvalidTaxDefinitionCalculationBehaviorError,
|
||||
isInvalidTaxDefinitionCodeError,
|
||||
isInvalidTaxDefinitionDescriptionError,
|
||||
isInvalidTaxDefinitionFamilyError,
|
||||
isInvalidTaxDefinitionIdError,
|
||||
isInvalidTaxDefinitionInvoiceNoteError,
|
||||
isInvalidTaxDefinitionNameError,
|
||||
isInvalidTaxDefinitionRateError,
|
||||
isInvalidTaxDefinitionScopeError,
|
||||
isInvalidTaxDefinitionValidityPeriodError,
|
||||
isTaxDefinitionCannotBeDisabledError,
|
||||
isTaxDefinitionCannotBeEnabledError,
|
||||
} from "../../../domain/tax-definitions";
|
||||
|
||||
const invalidTaxDefinitionIdRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionIdError,
|
||||
build: (error) =>
|
||||
new ConflictApiError(
|
||||
(error as InvalidTaxDefinitionIdError).message ||
|
||||
"Tax definition with the provided id already exists."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionCodeRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionCodeError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionCodeError).message || "Tax definition code is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionNameRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionNameError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionNameError).message || "Tax definition name is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionDescriptionRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionDescriptionError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionDescriptionError).message ||
|
||||
"Tax definition description is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionRateRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionRateError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionRateError).message || "Tax definition rate is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionFamilyRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionFamilyError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionFamilyError).message || "Tax definition family is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionCalculationBehaviorRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionCalculationBehaviorError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionCalculationBehaviorError).message ||
|
||||
"Tax definition calculation behavior is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionScopeRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionScopeError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionScopeError).message || "Tax definition scope is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionInvoiceNoteRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionInvoiceNoteError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionInvoiceNoteError).message ||
|
||||
"Tax definition invoice note is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionAllowedSurchargeCodesRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionAllowedSurchargeCodesError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionAllowedSurchargeCodesError).message ||
|
||||
"Tax definition allowed surcharge codes are invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidTaxDefinitionValidityPeriodRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isInvalidTaxDefinitionValidityPeriodError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidTaxDefinitionValidityPeriodError).message ||
|
||||
"Tax definition validity period is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const taxDefinitionNotFoundRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (error) =>
|
||||
error instanceof EntityNotFoundError &&
|
||||
((error as EntityNotFoundError).message.includes("TaxDefinition") ||
|
||||
(error as EntityNotFoundError).message.includes("Tax definition")),
|
||||
build: (error) =>
|
||||
new NotFoundApiError((error as EntityNotFoundError).message || "Tax definition not found."),
|
||||
};
|
||||
|
||||
const taxDefinitionCannotBeDisabledRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isTaxDefinitionCannotBeDisabledError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as TaxDefinitionCannotBeDisabledError).message || "Tax definition cannot be disabled."
|
||||
),
|
||||
};
|
||||
|
||||
const taxDefinitionCannotBeEnabledRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: isTaxDefinitionCannotBeEnabledError,
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as TaxDefinitionCannotBeEnabledError).message || "Tax definition cannot be enabled."
|
||||
),
|
||||
};
|
||||
|
||||
export const taxDefinitionsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invalidTaxDefinitionIdRule)
|
||||
.register(invalidTaxDefinitionCodeRule)
|
||||
.register(invalidTaxDefinitionNameRule)
|
||||
.register(invalidTaxDefinitionDescriptionRule)
|
||||
.register(invalidTaxDefinitionRateRule)
|
||||
.register(invalidTaxDefinitionFamilyRule)
|
||||
.register(invalidTaxDefinitionCalculationBehaviorRule)
|
||||
.register(invalidTaxDefinitionScopeRule)
|
||||
.register(invalidTaxDefinitionInvoiceNoteRule)
|
||||
.register(invalidTaxDefinitionAllowedSurchargeCodesRule)
|
||||
.register(invalidTaxDefinitionValidityPeriodRule)
|
||||
.register(taxDefinitionNotFoundRule)
|
||||
.register(taxDefinitionCannotBeDisabledRule)
|
||||
.register(taxDefinitionCannotBeEnabledRule);
|
||||
@ -0,0 +1,120 @@
|
||||
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { Router } from "express";
|
||||
|
||||
import {
|
||||
CreateTaxDefinitionRequestSchema,
|
||||
DeleteTaxDefinitionByIdRequestSchema,
|
||||
GetTaxDefinitionByIdRequestSchema,
|
||||
ListTaxDefinitionsRequestSchema,
|
||||
UpdateTaxDefinitionByIdParamsRequestSchema,
|
||||
UpdateTaxDefinitionByIdRequestSchema,
|
||||
} from "../../../../common";
|
||||
import type { CatalogsInternalDeps } from "../../di";
|
||||
|
||||
import {
|
||||
CreateTaxDefinitionController,
|
||||
DeleteTaxDefinitionByIdController,
|
||||
DisableTaxDefinitionByIdController,
|
||||
EnableTaxDefinitionByIdController,
|
||||
GetTaxDefinitionByIdController,
|
||||
ListTaxDefinitionsController,
|
||||
UpdateTaxDefinitionByIdController,
|
||||
} from "./controllers";
|
||||
|
||||
export const taxDefinitionsRouter = (params: StartParams) => {
|
||||
const { app, config, getInternal } = params;
|
||||
const deps = getInternal<CatalogsInternalDeps>("catalogs").taxDefinitions;
|
||||
|
||||
const router = Router({ mergeParams: true });
|
||||
|
||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||
router.use((req: Request, res: Response, next: NextFunction) =>
|
||||
mockUser(req as RequestWithAuth, res, next)
|
||||
);
|
||||
}
|
||||
|
||||
router.use([
|
||||
(req: Request, res: Response, next: NextFunction) =>
|
||||
requireAuthenticated()(req as RequestWithAuth, res, next),
|
||||
(req: Request, res: Response, next: NextFunction) =>
|
||||
requireCompanyContext()(req as RequestWithAuth, res, next),
|
||||
]);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
validateRequest(ListTaxDefinitionsRequestSchema, "query"),
|
||||
(req, res, next) => {
|
||||
const controller = new ListTaxDefinitionsController(deps.useCases.listTaxDefinitions());
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
validateRequest(CreateTaxDefinitionRequestSchema, "body"),
|
||||
(req, res, next) => {
|
||||
const controller = new CreateTaxDefinitionController(deps.useCases.createTaxDefinition());
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:tax_definition_id",
|
||||
validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
|
||||
(req, res, next) => {
|
||||
const controller = new GetTaxDefinitionByIdController(
|
||||
deps.useCases.getTaxDefinitionById()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:tax_definition_id",
|
||||
validateRequest(DeleteTaxDefinitionByIdRequestSchema, "params"),
|
||||
(req, res, next) => {
|
||||
const controller = new DeleteTaxDefinitionByIdController(
|
||||
deps.useCases.deleteTaxDefinitionById()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:tax_definition_id",
|
||||
validateRequest(UpdateTaxDefinitionByIdParamsRequestSchema, "params"),
|
||||
validateRequest(UpdateTaxDefinitionByIdRequestSchema, "body"),
|
||||
(req, res, next) => {
|
||||
const controller = new UpdateTaxDefinitionByIdController(
|
||||
deps.useCases.updateTaxDefinitionById()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:tax_definition_id/disable",
|
||||
validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
|
||||
(req, res, next) => {
|
||||
const controller = new DisableTaxDefinitionByIdController(
|
||||
deps.useCases.disableTaxDefinitionById()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:tax_definition_id/enable",
|
||||
validateRequest(GetTaxDefinitionByIdRequestSchema, "params"),
|
||||
(req, res, next) => {
|
||||
const controller = new EnableTaxDefinitionByIdController(
|
||||
deps.useCases.enableTaxDefinitionById()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
app.use(`${config.server.apiBasePath}/catalogs/tax-definitions`, router);
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./express";
|
||||
export * from "./di";
|
||||
export * from "./persistence";
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { SequelizeQueryMapper } from "@erp/core/api";
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
CountryCode,
|
||||
CountryRegionCode,
|
||||
TextValue,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
@ -15,18 +17,20 @@ import {
|
||||
TaxDefinitionCode as TaxDefinitionCodeVO,
|
||||
TaxDefinitionName as TaxDefinitionNameVO,
|
||||
TaxFamily,
|
||||
TaxJurisdictionCountryCode,
|
||||
TaxJurisdictionRegionCode,
|
||||
TaxRate as TaxRateVO,
|
||||
TaxScope,
|
||||
} from "../../../../../domain";
|
||||
import type { TaxDefinitionModel } from "../models";
|
||||
import type { TaxDefinitionCreationAttributes, TaxDefinitionModel } from "../models";
|
||||
|
||||
export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
|
||||
export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
||||
TaxDefinitionModel,
|
||||
TaxDefinitionCreationAttributes,
|
||||
TaxDefinition
|
||||
> {
|
||||
public mapToDomain(raw: TaxDefinitionModel): Result<TaxDefinition, Error> {
|
||||
public mapToDomain(
|
||||
raw: TaxDefinitionModel,
|
||||
params?: MapperParamsType
|
||||
): Result<TaxDefinition, Error> {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||
@ -51,13 +55,13 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
|
||||
);
|
||||
|
||||
const jurisdictionCountryCode = extractOrPushError(
|
||||
TaxJurisdictionCountryCode.create(raw.jurisdiction_country_code),
|
||||
CountryCode.create(raw.jurisdiction_country_code),
|
||||
"jurisdiction_country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) =>
|
||||
TaxJurisdictionRegionCode.create(v)
|
||||
CountryRegionCode.create(v)
|
||||
);
|
||||
|
||||
const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors);
|
||||
@ -69,9 +73,10 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
|
||||
|
||||
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());
|
||||
const _result = TaxDefinitionCodeVO.create(String(el));
|
||||
if (_result.isFailure)
|
||||
return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
|
||||
arr.push(_result.data);
|
||||
}
|
||||
|
||||
return Result.ok(arr);
|
||||
@ -112,46 +117,51 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeQueryMapper<
|
||||
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(
|
||||
public mapToPersistence(
|
||||
source: TaxDefinition,
|
||||
params?: MapperParamsType
|
||||
): Result<TaxDefinitionCreationAttributes, Error> {
|
||||
const dto: TaxDefinitionCreationAttributes = {
|
||||
id: source.id.toPrimitive(),
|
||||
company_id: source.companyId.toPrimitive(),
|
||||
code: source.code.toPrimitive(),
|
||||
name: source.name.toPrimitive(),
|
||||
description: source.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(
|
||||
rate_value: source.rate.toPrimitive(),
|
||||
rate_scale: (source.rate as any).scale ?? 2,
|
||||
tax_family: source.taxFamily.toPrimitive(),
|
||||
calculation_behavior: source.calculationBehavior.toPrimitive(),
|
||||
jurisdiction_country_code: source.jurisdictionCountryCode.toPrimitive(),
|
||||
jurisdiction_region_code: source.jurisdictionRegionCode.match(
|
||||
(v) => v.toPrimitive(),
|
||||
() => null
|
||||
),
|
||||
tax_scope: domain.taxScope.toPrimitive(),
|
||||
invoice_note: domain.invoiceNote.match(
|
||||
tax_scope: source.taxScope.toPrimitive(),
|
||||
invoice_note: source.invoiceNote.match(
|
||||
(v) => v.toPrimitive(),
|
||||
() => null
|
||||
),
|
||||
allowed_surcharge_codes: domain.allowedSurchargeCodes.match(
|
||||
allowed_surcharge_codes: source.allowedSurchargeCodes.match(
|
||||
(arr) => arr.map((c) => c.toPrimitive()),
|
||||
() => null
|
||||
),
|
||||
is_system: domain.isSystem,
|
||||
is_active: domain.isActive,
|
||||
valid_from: domain.validFrom.match(
|
||||
is_system: source.isSystem,
|
||||
is_active: source.isActive,
|
||||
valid_from: source.validFrom.match(
|
||||
(d) => d.toPrimitive(),
|
||||
() => null
|
||||
),
|
||||
valid_to: domain.validTo.match(
|
||||
valid_to: source.validTo.match(
|
||||
(d) => d.toPrimitive(),
|
||||
() => null
|
||||
),
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok<TaxDefinitionCreationAttributes>(
|
||||
dto satisfies TaxDefinitionCreationAttributes
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-tax-definition.model";
|
||||
@ -5,9 +5,9 @@ import {
|
||||
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 { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Op, type Sequelize, type Transaction, type WhereOptions } from "sequelize";
|
||||
|
||||
import type { ITaxDefinitionRepository, TaxDefinitionSummary } from "../../../../../application";
|
||||
import type { TaxDefinition, TaxDefinitionCode } from "../../../../../domain";
|
||||
@ -123,6 +123,62 @@ export class SequelizeTaxDefinitionRepository
|
||||
}
|
||||
}
|
||||
|
||||
async findActiveByCodeInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
code: string;
|
||||
atDate: UtcDate;
|
||||
transaction?: Transaction;
|
||||
}): Promise<Result<Maybe<TaxDefinition>, Error>> {
|
||||
try {
|
||||
const row = await TaxDefinitionModel.findOne({
|
||||
where: {
|
||||
...this.buildActiveWhereClause(params.companyId, params.atDate),
|
||||
code: this.normalizeCode(params.code),
|
||||
},
|
||||
transaction: params.transaction,
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
return Result.ok(Maybe.none<TaxDefinition>());
|
||||
}
|
||||
|
||||
return this.domainMapper.mapToDomain(row).map((taxDefinition) => Maybe.some(taxDefinition));
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async findActiveByCodesInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
codes: Collection<string>;
|
||||
atDate: UtcDate;
|
||||
transaction?: Transaction;
|
||||
}): Promise<Result<Collection<TaxDefinition>, Error>> {
|
||||
try {
|
||||
const normalizedCodes = Array.from(
|
||||
new Set(params.codes.getAll().map((code) => this.normalizeCode(code)))
|
||||
);
|
||||
|
||||
if (normalizedCodes.length === 0) {
|
||||
return Result.ok(new Collection([]));
|
||||
}
|
||||
|
||||
const rows = await TaxDefinitionModel.findAll({
|
||||
where: {
|
||||
...this.buildActiveWhereClause(params.companyId, params.atDate),
|
||||
code: {
|
||||
[Op.in]: normalizedCodes,
|
||||
},
|
||||
},
|
||||
transaction: params.transaction,
|
||||
});
|
||||
|
||||
return this.domainMapper.mapToDomainCollection(rows, rows.length);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
@ -171,4 +227,25 @@ export class SequelizeTaxDefinitionRepository
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
private buildActiveWhereClause(companyId: UniqueID, atDate: UtcDate): WhereOptions {
|
||||
const atDateISO = atDate.toPrimitive();
|
||||
|
||||
return {
|
||||
company_id: companyId.toString(),
|
||||
is_active: true,
|
||||
[Op.and]: [
|
||||
{
|
||||
[Op.or]: [{ valid_from: null }, { valid_from: { [Op.lte]: atDateISO } }],
|
||||
},
|
||||
{
|
||||
[Op.or]: [{ valid_to: null }, { valid_to: { [Op.gte]: atDateISO } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeCode(code: string): string {
|
||||
return code.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,7 @@ import type { ModuleParams, SetupParams } from "@erp/core/api";
|
||||
import { buildTransactionManager } from "@erp/core/api";
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import type { ITaxRegimeRepository } from "../../../application/";
|
||||
import { TaxRegimeFinder } from "../../../application/";
|
||||
import { type ITaxRegimeRepository, TaxRegimePublicFinder } from "../../../application/";
|
||||
import {
|
||||
CreateTaxRegimeUseCase,
|
||||
DeleteTaxRegimeByIdUseCase,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
EnableTaxRegimeByIdUseCase,
|
||||
GetTaxRegimeByIdUseCase,
|
||||
ListTaxRegimesUseCase,
|
||||
TaxRegimePublicModelMapper,
|
||||
UpdateTaxRegimeByIdUseCase,
|
||||
} from "../../../application/tax-regimes";
|
||||
import {
|
||||
@ -112,8 +112,13 @@ export const buildTaxRegimesDependencies = (params: ModuleParams): TaxRegimesInt
|
||||
export const buildTaxRegimesPublicServices = (
|
||||
_params: SetupParams,
|
||||
deps: TaxRegimesInternalDeps
|
||||
): { finder: TaxRegimeFinder } => {
|
||||
): { finder: TaxRegimePublicFinder } => {
|
||||
const mapper = new TaxRegimePublicModelMapper();
|
||||
|
||||
return {
|
||||
finder: new TaxRegimeFinder(deps.repository),
|
||||
finder: new TaxRegimePublicFinder({
|
||||
repository: deps.repository,
|
||||
mapper,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,7 +37,7 @@ export default (database: Sequelize) => {
|
||||
allowNull: false,
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
type: DataTypes.STRING(2),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
@ -6,8 +6,8 @@ import {
|
||||
} 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 { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Op, type Sequelize, type Transaction } from "sequelize";
|
||||
|
||||
import type { ITaxRegimeRepository, TaxRegimeSummary } from "../../../../../application";
|
||||
import type { TaxRegime, TaxRegimeCode } from "../../../../../domain";
|
||||
@ -105,6 +105,22 @@ export class SequelizeTaxRegimeRepository
|
||||
}
|
||||
}
|
||||
|
||||
async existsByCodeInCompany(
|
||||
companyId: UniqueID,
|
||||
code: TaxRegimeCode,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const count = await TaxRegimeModel.count({
|
||||
where: { code: code.toString(), company_id: companyId.toString() },
|
||||
transaction,
|
||||
});
|
||||
return Result.ok(Boolean(count > 0));
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera un método de pago por su ID y companyId.
|
||||
*
|
||||
@ -250,4 +266,55 @@ export class SequelizeTaxRegimeRepository
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async findByCodeInCompany(
|
||||
companyId: UniqueID,
|
||||
code: TaxRegimeCode,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Maybe<TaxRegime>, Error>> {
|
||||
try {
|
||||
const row = await TaxRegimeModel.findOne({
|
||||
where: { code: code.toString(), company_id: companyId.toString() },
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
return Result.ok(Maybe.none<TaxRegime>());
|
||||
}
|
||||
|
||||
return this.domainMapper.mapToDomain(row).map((taxDefinition) => Maybe.some(taxDefinition));
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async findByCodesInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
codes: Collection<TaxRegimeCode>;
|
||||
transaction?: Transaction;
|
||||
}): Promise<Result<Collection<TaxRegime>, Error>> {
|
||||
try {
|
||||
const normalizedCodes = Array.from(
|
||||
new Set(params.codes.getAll().map((code) => code.toString()))
|
||||
);
|
||||
|
||||
if (normalizedCodes.length === 0) {
|
||||
return Result.ok(new Collection());
|
||||
}
|
||||
|
||||
const rows = await TaxRegimeModel.findAll({
|
||||
where: {
|
||||
company_id: params.companyId.toString(),
|
||||
code: {
|
||||
[Op.in]: normalizedCodes,
|
||||
},
|
||||
},
|
||||
transaction: params.transaction,
|
||||
});
|
||||
|
||||
return this.domainMapper.mapToDomainCollection(rows, rows.length);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./payment-methods";
|
||||
export * from "./payment-terms";
|
||||
export * from "./tax-definitions";
|
||||
export * from "./tax-regimes";
|
||||
|
||||
3
modules/catalogs/src/common/dto/tax-definitions/index.ts
Normal file
3
modules/catalogs/src/common/dto/tax-definitions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./request";
|
||||
export * from "./response";
|
||||
export * from "./shared";
|
||||
@ -0,0 +1,43 @@
|
||||
import { IsoDateSchema, PercentageSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
|
||||
const TaxDefinitionFamilySchema = z.enum([
|
||||
"iva",
|
||||
"igic",
|
||||
"ipsi",
|
||||
"equivalence_surcharge",
|
||||
"withholding",
|
||||
"vat",
|
||||
"gst",
|
||||
"sales_tax",
|
||||
"reverse_charge",
|
||||
"exempt",
|
||||
"not_subject",
|
||||
"custom",
|
||||
]);
|
||||
const TaxCalculationBehaviorSchema = z.enum(["additive", "subtractive", "neutral"]);
|
||||
const TaxScopeSchema = z.enum(["domestic", "intra_eu", "export", "import", "international"]);
|
||||
const JurisdictionCountryCodeSchema = z.string().regex(/^[A-Z]{2}$/);
|
||||
const JurisdictionRegionCodeSchema = z.string().regex(/^[A-Z]{2}-[A-Z0-9-]+$/);
|
||||
|
||||
export const CreateTaxDefinitionRequestSchema = z.object({
|
||||
id: z.uuid(),
|
||||
code: TaxDefinitionCodeSchema,
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
rate: PercentageSchema,
|
||||
tax_family: TaxDefinitionFamilySchema,
|
||||
calculation_behavior: TaxCalculationBehaviorSchema,
|
||||
jurisdiction_country_code: JurisdictionCountryCodeSchema,
|
||||
jurisdiction_region_code: JurisdictionRegionCodeSchema.nullable().optional(),
|
||||
tax_scope: TaxScopeSchema,
|
||||
invoice_note: z.string().nullable().optional(),
|
||||
allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable().optional(),
|
||||
is_system: z.boolean().optional(),
|
||||
is_active: z.boolean(),
|
||||
valid_from: IsoDateSchema.nullable().optional(),
|
||||
valid_to: IsoDateSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type CreateTaxDefinitionRequestDTO = z.infer<typeof CreateTaxDefinitionRequestSchema>;
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const DeleteTaxDefinitionByIdRequestSchema = z.object({
|
||||
tax_definition_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type DeleteTaxDefinitionByIdRequestDTO = z.infer<
|
||||
typeof DeleteTaxDefinitionByIdRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const DisableTaxDefinitionByIdRequestSchema = z.object({
|
||||
tax_definition_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type DisableTaxDefinitionByIdRequestDTO = z.infer<
|
||||
typeof DisableTaxDefinitionByIdRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const EnableTaxDefinitionByIdRequestSchema = z.object({
|
||||
tax_definition_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type EnableTaxDefinitionByIdRequestDTO = z.infer<
|
||||
typeof EnableTaxDefinitionByIdRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,7 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const GetTaxDefinitionByIdRequestSchema = z.object({
|
||||
tax_definition_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type GetTaxDefinitionByIdRequestDTO = z.infer<typeof GetTaxDefinitionByIdRequestSchema>;
|
||||
@ -0,0 +1,7 @@
|
||||
export * from "./create-tax-definition.request.dto";
|
||||
export * from "./delete-tax-definition-by-id.request.dto";
|
||||
export * from "./disable-tax-definition-by-id.request.dto";
|
||||
export * from "./enable-tax-definition-by-id.request.dto";
|
||||
export * from "./get-tax-definition-by-id.request.dto";
|
||||
export * from "./list-tax-definitions.request.dto";
|
||||
export * from "./update-tax-definition-by-id.request.dto";
|
||||
@ -0,0 +1,5 @@
|
||||
import { CriteriaSchema } from "@erp/core";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const ListTaxDefinitionsRequestSchema = CriteriaSchema;
|
||||
export type ListTaxDefinitionsRequestDTO = z.infer<typeof ListTaxDefinitionsRequestSchema>;
|
||||
@ -0,0 +1,26 @@
|
||||
import { IsoDateSchema, PercentageSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
|
||||
|
||||
export const UpdateTaxDefinitionByIdParamsRequestSchema = z.object({
|
||||
tax_definition_id: z.uuid(),
|
||||
});
|
||||
|
||||
export const UpdateTaxDefinitionByIdRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
rate: PercentageSchema.optional(),
|
||||
invoice_note: z.string().nullable().optional(),
|
||||
allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable().optional(),
|
||||
is_active: z.boolean().optional(),
|
||||
valid_from: IsoDateSchema.nullable().optional(),
|
||||
valid_to: IsoDateSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateTaxDefinitionByIdParamsRequestDTO = z.infer<
|
||||
typeof UpdateTaxDefinitionByIdParamsRequestSchema
|
||||
>;
|
||||
export type UpdateTaxDefinitionByIdRequestDTO = z.infer<
|
||||
typeof UpdateTaxDefinitionByIdRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,6 @@
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionDetailSchema } from "../shared";
|
||||
|
||||
export const CreateTaxDefinitionResponseSchema = TaxDefinitionDetailSchema;
|
||||
export type CreateTaxDefinitionResponseDTO = z.infer<typeof CreateTaxDefinitionResponseSchema>;
|
||||
@ -0,0 +1,8 @@
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionDetailSchema } from "../shared";
|
||||
|
||||
export const DisableTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
|
||||
export type DisableTaxDefinitionByIdResponseDTO = z.infer<
|
||||
typeof DisableTaxDefinitionByIdResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,8 @@
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionDetailSchema } from "../shared";
|
||||
|
||||
export const EnableTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
|
||||
export type EnableTaxDefinitionByIdResponseDTO = z.infer<
|
||||
typeof EnableTaxDefinitionByIdResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,8 @@
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionDetailSchema } from "../shared";
|
||||
|
||||
export const GetTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
|
||||
export type GetTaxDefinitionByIdResponseDTO = z.infer<
|
||||
typeof GetTaxDefinitionByIdResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,6 @@
|
||||
export * from "./create-tax-definition.response.dto";
|
||||
export * from "./disable-tax-definition-by-id.response.dto";
|
||||
export * from "./enable-tax-definition-by-id.response.dto";
|
||||
export * from "./get-tax-definition-by-id.response.dto";
|
||||
export * from "./list-tax-definitions.response.dto";
|
||||
export * from "./update-tax-definition-by-id.response.dto";
|
||||
@ -0,0 +1,9 @@
|
||||
import { createPaginatedListSchema } from "@erp/core";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionSummarySchema } from "../shared";
|
||||
|
||||
export const ListTaxDefinitionsResponseSchema = createPaginatedListSchema(
|
||||
TaxDefinitionSummarySchema
|
||||
);
|
||||
export type ListTaxDefinitionsResponseDTO = z.infer<typeof ListTaxDefinitionsResponseSchema>;
|
||||
@ -0,0 +1,8 @@
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { TaxDefinitionDetailSchema } from "../shared";
|
||||
|
||||
export const UpdateTaxDefinitionByIdResponseSchema = TaxDefinitionDetailSchema;
|
||||
export type UpdateTaxDefinitionByIdResponseDTO = z.infer<
|
||||
typeof UpdateTaxDefinitionByIdResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./tax-definition-detail.dto";
|
||||
export * from "./tax-definition-summary.dto";
|
||||
@ -0,0 +1,44 @@
|
||||
import { IsoDateSchema, PercentageSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
|
||||
const TaxDefinitionFamilySchema = z.enum([
|
||||
"iva",
|
||||
"igic",
|
||||
"ipsi",
|
||||
"equivalence_surcharge",
|
||||
"withholding",
|
||||
"vat",
|
||||
"gst",
|
||||
"sales_tax",
|
||||
"reverse_charge",
|
||||
"exempt",
|
||||
"not_subject",
|
||||
"custom",
|
||||
]);
|
||||
const TaxCalculationBehaviorSchema = z.enum(["additive", "subtractive", "neutral"]);
|
||||
const TaxScopeSchema = z.enum(["domestic", "intra_eu", "export", "import", "international"]);
|
||||
const JurisdictionCountryCodeSchema = z.string().regex(/^[A-Z]{2}$/);
|
||||
const JurisdictionRegionCodeSchema = z.string().regex(/^[A-Z]{2}-[A-Z0-9-]+$/);
|
||||
|
||||
export const TaxDefinitionDetailSchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
code: TaxDefinitionCodeSchema,
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
rate: PercentageSchema,
|
||||
tax_family: TaxDefinitionFamilySchema,
|
||||
calculation_behavior: TaxCalculationBehaviorSchema,
|
||||
jurisdiction_country_code: JurisdictionCountryCodeSchema,
|
||||
jurisdiction_region_code: JurisdictionRegionCodeSchema.nullable(),
|
||||
tax_scope: TaxScopeSchema,
|
||||
invoice_note: z.string().nullable(),
|
||||
allowed_surcharge_codes: z.array(TaxDefinitionCodeSchema).nullable(),
|
||||
is_system: z.boolean(),
|
||||
is_active: z.boolean(),
|
||||
valid_from: IsoDateSchema.nullable(),
|
||||
valid_to: IsoDateSchema.nullable(),
|
||||
});
|
||||
|
||||
export type TaxDefinitionDetailDTO = z.infer<typeof TaxDefinitionDetailSchema>;
|
||||
@ -0,0 +1,14 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const TaxDefinitionCodeSchema = z.string().regex(/^[a-z0-9][a-z0-9_]*$/);
|
||||
|
||||
export const TaxDefinitionSummarySchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
code: TaxDefinitionCodeSchema,
|
||||
name: z.string(),
|
||||
is_active: z.boolean(),
|
||||
is_system: z.boolean(),
|
||||
});
|
||||
|
||||
export type TaxDefinitionSummaryDTO = z.infer<typeof TaxDefinitionSummarySchema>;
|
||||
@ -1,4 +1,4 @@
|
||||
export type ISnapshotBuilderParams = Readonly<Record<string, unknown>>;
|
||||
export type ISnapshotBuilderParams = {}; //Readonly<Record<string, unknown>>;
|
||||
|
||||
export interface ISnapshotBuilder<TSource, TSnapshot = unknown> {
|
||||
toOutput(source: TSource, params?: ISnapshotBuilderParams): TSnapshot;
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
type TaxPercentageProps = Pick<PercentageProps, "value">;
|
||||
|
||||
export class TaxPercentage extends Percentage {
|
||||
static DEFAULT_SCALE = 2;
|
||||
public static readonly DEFAULT_SCALE = 2;
|
||||
|
||||
static create({ value }: TaxPercentageProps): Result<Percentage> {
|
||||
return Percentage.create({
|
||||
public static create({ value }: TaxPercentageProps): Result<TaxPercentage> {
|
||||
const result = Percentage.create({
|
||||
value,
|
||||
scale: TaxPercentage.DEFAULT_SCALE,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return Result.ok(new TaxPercentage(result.data.getProps()));
|
||||
}
|
||||
|
||||
static zero() {
|
||||
public static zero(): TaxPercentage {
|
||||
return TaxPercentage.create({ value: 0 }).data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,178 +1,183 @@
|
||||
import type { TaxCatalogProvider } from "@erp/core";
|
||||
import { ValueObject } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { TaxPercentage } from "./tax-percentage.vo";
|
||||
import { TaxPercentage } from "./tax-percentage.vo.js";
|
||||
|
||||
const TAX_GROUPS = ["IVA", "IPSI", "IGIC", "retention", "rec"] as const;
|
||||
type TaxGroup = (typeof TAX_GROUPS)[number];
|
||||
export const TAX_GROUPS = ["iva", "ipsi", "igic", "retention", "surcharge"] as const;
|
||||
|
||||
export type TaxGroup = (typeof TAX_GROUPS)[number];
|
||||
|
||||
export type TaxCalculationBehavior = "additive" | "subtractive";
|
||||
|
||||
export interface TaxProps {
|
||||
code: string; // iva_21
|
||||
name: string; // 21% IVA
|
||||
value: number; // 2100
|
||||
code: string;
|
||||
name: string;
|
||||
rate: TaxPercentage;
|
||||
group: TaxGroup;
|
||||
calculationBehavior: TaxCalculationBehavior;
|
||||
}
|
||||
|
||||
export interface CreateTaxProps {
|
||||
code: string;
|
||||
name: string;
|
||||
rate: TaxPercentage;
|
||||
group: TaxGroup;
|
||||
calculationBehavior: TaxCalculationBehavior;
|
||||
}
|
||||
|
||||
export class Tax extends ValueObject<TaxProps> {
|
||||
static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
|
||||
static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
|
||||
static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
|
||||
static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
|
||||
static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
|
||||
public static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
|
||||
public static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
|
||||
public static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
|
||||
public static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
|
||||
public static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
|
||||
|
||||
private static CODE_REGEX = /^[a-z0-9_:-]+$/;
|
||||
private static readonly CODE_REGEX = /^[a-z0-9_:-]+$/;
|
||||
|
||||
private _percentage!: TaxPercentage;
|
||||
public static create(props: CreateTaxProps): Result<Tax> {
|
||||
const validationResult = Tax.validate(props);
|
||||
|
||||
protected static validate(values: TaxProps) {
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(
|
||||
new Error(validationResult.error.issues.map((issue) => issue.message).join(", "))
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
new Tax({
|
||||
code: props.code.trim(),
|
||||
name: props.name.trim(),
|
||||
rate: props.rate,
|
||||
group: props.group,
|
||||
calculationBehavior: props.calculationBehavior,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected static validate(values: CreateTaxProps) {
|
||||
const schema = z.object({
|
||||
value: z
|
||||
.number()
|
||||
.int()
|
||||
.min(Tax.MIN_VALUE, "La tasa de impuesto no puede ser negativa.")
|
||||
.max(Tax.MAX_VALUE * 10 ** Tax.MAX_SCALE, "La tasa de impuesto es demasiado alta."),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "El nombre del impuesto es obligatorio.")
|
||||
.max(100, "El nombre del impuesto no puede exceder 100 caracteres."),
|
||||
|
||||
code: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "El código del impuesto es obligatorio.")
|
||||
.max(40, "El código del impuesto no puede exceder 40 caracteres.")
|
||||
.regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos."),
|
||||
group: z.enum(TAX_GROUPS, "El impuesto debe ser un IVA, retención o rec. equivalencia"),
|
||||
|
||||
group: z.enum(TAX_GROUPS),
|
||||
|
||||
calculationBehavior: z.enum(["additive", "subtractive"]),
|
||||
});
|
||||
|
||||
return schema.safeParse(values);
|
||||
}
|
||||
|
||||
static create(props: TaxProps): Result<Tax> {
|
||||
const { value, name, code, group } = props;
|
||||
|
||||
const validationResult = Tax.validate({ value, name, code, group });
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
|
||||
}
|
||||
|
||||
return Result.ok(new Tax({ value, name, code, group }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un Tax usando solo el 'code', resolviendo el resto de datos desde el catálogo.
|
||||
* @param code Código del impuesto (p.ej. "iva_21")
|
||||
* @param provider Proveedor del catálogo de impuestos
|
||||
*/
|
||||
static createFromCode(code: string, provider: TaxCatalogProvider): Result<Tax, Error> {
|
||||
const normalized = (code ?? "").trim().toLowerCase();
|
||||
|
||||
const schema = z
|
||||
.string()
|
||||
.min(1, "El código del impuesto es obligatorio.")
|
||||
.max(40, "El código del impuesto no puede exceder 40 caracteres.")
|
||||
.regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos.");
|
||||
|
||||
const validationResult = schema.safeParse(normalized);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
|
||||
}
|
||||
|
||||
const maybeItem = provider.findByCode(normalized);
|
||||
if (maybeItem.isNone()) {
|
||||
return Result.fail(
|
||||
new Error(`Código de impuesto no encontrado en el catálogo: "${normalized}"`)
|
||||
);
|
||||
}
|
||||
|
||||
const item = maybeItem.unwrap();
|
||||
|
||||
// Delegamos en create para reusar validación y límites
|
||||
return Tax.create({
|
||||
value: Number(item.value),
|
||||
name: item.name,
|
||||
code: item.code, // guardamos el code tal cual viene del catálogo
|
||||
group: item.group as TaxGroup,
|
||||
});
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this.props.value;
|
||||
}
|
||||
get scale(): number {
|
||||
return Tax.DEFAULT_SCALE;
|
||||
}
|
||||
get name(): string {
|
||||
return this.props.name;
|
||||
}
|
||||
get code(): string {
|
||||
public get code(): string {
|
||||
return this.props.code;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
public get name(): string {
|
||||
return this.props.name;
|
||||
}
|
||||
|
||||
public get rate(): TaxPercentage {
|
||||
return this.props.rate;
|
||||
}
|
||||
|
||||
public get group(): TaxGroup {
|
||||
return this.props.group;
|
||||
}
|
||||
|
||||
get percentage(): TaxPercentage {
|
||||
return TaxPercentage.create({ value: this.value }).data;
|
||||
public get calculationBehavior(): TaxCalculationBehavior {
|
||||
return this.props.calculationBehavior;
|
||||
}
|
||||
|
||||
isVATLike(): boolean {
|
||||
return this.group === "IVA" || this.group === "IGIC" || this.group === "IPSI";
|
||||
public get value(): number {
|
||||
return this.props.rate.value;
|
||||
}
|
||||
|
||||
isRetention(): boolean {
|
||||
return this.group === "retention";
|
||||
public get scale(): number {
|
||||
return this.props.rate.scale;
|
||||
}
|
||||
|
||||
isRec(): boolean {
|
||||
return this.group === "rec";
|
||||
public get percentage(): TaxPercentage {
|
||||
return this.props.rate;
|
||||
}
|
||||
|
||||
getProps(): TaxProps {
|
||||
public isAdditive(): boolean {
|
||||
return this.props.calculationBehavior === "additive";
|
||||
}
|
||||
|
||||
public isSubtractive(): boolean {
|
||||
return this.props.calculationBehavior === "subtractive";
|
||||
}
|
||||
|
||||
public isIva(): boolean {
|
||||
return this.props.group === "iva";
|
||||
}
|
||||
|
||||
public isIgic(): boolean {
|
||||
return this.props.group === "igic";
|
||||
}
|
||||
|
||||
public isIpsi(): boolean {
|
||||
return this.props.group === "ipsi";
|
||||
}
|
||||
|
||||
public isSurcharge(): boolean {
|
||||
return this.props.group === "surcharge";
|
||||
}
|
||||
|
||||
public isRetention(): boolean {
|
||||
return this.props.group === "retention";
|
||||
}
|
||||
|
||||
public getProps(): TaxProps {
|
||||
return this.props;
|
||||
}
|
||||
|
||||
toPrimitive() {
|
||||
public toPrimitive(): TaxProps {
|
||||
return this.getProps();
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
public toNumber(): number {
|
||||
return this.value / 10 ** this.scale;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.toNumber().toFixed(this.scale)}%`;
|
||||
public toString(): string {
|
||||
return this.rate.toString();
|
||||
}
|
||||
|
||||
isZero(): boolean {
|
||||
public isZero(): boolean {
|
||||
return this.value === 0;
|
||||
}
|
||||
|
||||
isPositive(): boolean {
|
||||
public isPositive(): boolean {
|
||||
return this.value > 0;
|
||||
}
|
||||
|
||||
equalsTo(other: Tax): boolean {
|
||||
return this.equals(other);
|
||||
public equalsTo(other: Tax): boolean {
|
||||
return (
|
||||
this.code === other.code &&
|
||||
this.name === other.name &&
|
||||
this.value === other.value &&
|
||||
this.scale === other.scale &&
|
||||
this.group === other.group &&
|
||||
this.calculationBehavior === other.calculationBehavior
|
||||
);
|
||||
}
|
||||
greaterThan(other: Tax): boolean {
|
||||
|
||||
public greaterThan(other: Tax): boolean {
|
||||
return this.toNumber() > other.toNumber();
|
||||
}
|
||||
|
||||
lessThan(other: Tax): boolean {
|
||||
public lessThan(other: Tax): boolean {
|
||||
return this.toNumber() < other.toNumber();
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
value: this.value,
|
||||
scale: this.scale,
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
percentage: this.toNumber(),
|
||||
formatted: this.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
import {
|
||||
FactuGESPaymentCatalogProvider,
|
||||
type JsonPaymentCatalogProvider,
|
||||
type JsonTaxCatalogProvider,
|
||||
SpainTaxCatalogProvider,
|
||||
} from "../../../common";
|
||||
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "../../../common";
|
||||
|
||||
export interface ICatalogs {
|
||||
taxCatalog: JsonTaxCatalogProvider;
|
||||
paymentCatalog: JsonPaymentCatalogProvider;
|
||||
//paymentCatalog: JsonPaymentCatalogProvider;
|
||||
}
|
||||
|
||||
export const buildCatalogs = (): ICatalogs => {
|
||||
const taxCatalog = SpainTaxCatalogProvider();
|
||||
const paymentCatalog = FactuGESPaymentCatalogProvider();
|
||||
//const paymentCatalog = FactuGESPaymentCatalogProvider();
|
||||
|
||||
return {
|
||||
taxCatalog,
|
||||
paymentCatalog,
|
||||
//paymentCatalog,
|
||||
};
|
||||
};
|
||||
|
||||
@ -17,9 +17,8 @@ import {
|
||||
isDomainValidationError,
|
||||
isValidationErrorCollection,
|
||||
} from "@repo/rdx-ddd";
|
||||
import type { ZodError } from "zod";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { isSchemaError } from "../../../common/schemas";
|
||||
import { type DocumentGenerationError, isDocumentGenerationError } from "../../application";
|
||||
import {
|
||||
type DuplicateEntityError,
|
||||
@ -46,6 +45,8 @@ import {
|
||||
ValidationApiError,
|
||||
} from "./errors";
|
||||
|
||||
export const isSchemaError = (e: unknown): e is ZodError => e instanceof ZodError;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
// Contexto opcional para enriquecer Problem+JSON (útil en middleware Express)
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -12,7 +12,7 @@ export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttribut
|
||||
public abstract mapToPersistence(
|
||||
domain: TEntity,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes, Error>;
|
||||
): Result<TModelAttributes, Error> | Promise<Result<TModelAttributes, Error>>;
|
||||
|
||||
public mapToDomainCollection(
|
||||
raws: (TModel | TModelAttributes)[],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user