feat: Refactor tax handling in proforma and recurring proforma modules

- Introduced ProformaItemTax value object to encapsulate tax properties (code, name, percentage).
- Updated ProformaTaxResolver to utilize ProformaItemTax instead of Tax.
- Modified recurring proforma materializer to handle new tax structure.
- Adjusted proforma aggregate and item entities to accommodate ProformaItemTax.
- Enhanced Sequelize mappers to map to and from the new ProformaItemTax structure.
- Updated tests to reflect changes in tax handling and ensure correctness.
This commit is contained in:
David Arranz 2026-09-10 13:21:49 +02:00
parent e94b1dd7bd
commit 0a2e7795f1
19 changed files with 343 additions and 184 deletions

View File

@ -29,14 +29,17 @@ protección transversal consolidada que impida borrar una `TaxDefinition` por es
## Fiscalidad viva de Proforma ## Fiscalidad viva de Proforma
Una Proforma es fiscalmente viva. Al cargarla, si existe la `TaxDefinition` de la empresa para `Tax` es el objeto operacional obtenido del catálogo: contiene `code`, `name`, `rate`, `group`
un código, prevalece la definición actual: `code`, `name`, porcentaje/rate, family/group y y `calculationBehavior`. Antes de entrar en una línea de Proforma se convierte explícitamente a
`calculationBehavior`. Por ejemplo, una línea persistida como `IVA21` al 21 % se presenta al `ProformaItemTax`, el snapshot fiscal de la línea: `code`, `name` y `percentage`.
22 % si la definición viva `IVA21` se actualizó al 22 %.
La línea conserva además un respaldo persistido completo: código, nombre, porcentaje, group y Al cargar una Proforma, si existe la `TaxDefinition` para un código, prevalece su definición viva
calculation behavior (más los importes calculados). Ese respaldo no es fuente funcional primaria y se vuelve a materializar como `ProformaItemTax`. Si falta, se conserva el snapshot persistido
cuando existe catálogo vivo; sirve para recuperación, diagnóstico y lectura ante inconsistencias. (`code`, `name`, `percentage`) para que la Proforma siga siendo legible. Los importes no forman
parte de `ProformaItemTax`: se derivan siempre de `taxableAmount × percentage`.
`group` y `calculationBehavior` pertenecen exclusivamente a `Tax`/`TaxDefinition` operacional;
no se persisten en `ProformaItem`.
## Disponibilidad fiscal ## Disponibilidad fiscal
@ -45,7 +48,7 @@ La disponibilidad se evalúa por `companyId`, código fiscal y `proformaDate`:
- `available`: puede asignarse, cambiar de estado y emitirse. - `available`: puede asignarse, cambiar de estado y emitirse.
- `inactive` o `out_of_validity`: una lectura existente se resuelve live, pero no admite nueva - `inactive` o `out_of_validity`: una lectura existente se resuelve live, pero no admite nueva
asignación, cambio de estado ni emisión. asignación, cambio de estado ni emisión.
- `missing`: la lectura hace fallback al Tax persistido; no permite nueva asignación, cambio de - `missing`: la lectura hace fallback al `ProformaItemTax` persistido; no permite nueva asignación, cambio de
estado ni emisión. estado ni emisión.
No hay fallback a defaults de Customer o Company. Readiness expone blockers `tax_unavailable` No hay fallback a defaults de Customer o Company. Readiness expone blockers `tax_unavailable`

View File

@ -114,24 +114,18 @@ CREATE TABLE IF NOT EXISTS proforma_items (
taxable_amount_scale SMALLINT NOT NULL DEFAULT 4, taxable_amount_scale SMALLINT NOT NULL DEFAULT 4,
iva_code VARCHAR(40) NULL, iva_code VARCHAR(40) NULL,
iva_name VARCHAR(100) NULL, iva_name VARCHAR(100) NULL,
iva_group VARCHAR(40) NULL,
iva_calculation_behavior VARCHAR(40) NULL,
iva_percentage_value SMALLINT NULL, iva_percentage_value SMALLINT NULL,
iva_percentage_scale SMALLINT NOT NULL DEFAULT 2, iva_percentage_scale SMALLINT NOT NULL DEFAULT 2,
iva_amount_value BIGINT NOT NULL DEFAULT 0, iva_amount_value BIGINT NOT NULL DEFAULT 0,
iva_amount_scale SMALLINT NOT NULL DEFAULT 4, iva_amount_scale SMALLINT NOT NULL DEFAULT 4,
rec_code VARCHAR(40) NULL, rec_code VARCHAR(40) NULL,
rec_name VARCHAR(100) NULL, rec_name VARCHAR(100) NULL,
rec_group VARCHAR(40) NULL,
rec_calculation_behavior VARCHAR(40) NULL,
rec_percentage_value SMALLINT NULL, rec_percentage_value SMALLINT NULL,
rec_percentage_scale SMALLINT NOT NULL DEFAULT 2, rec_percentage_scale SMALLINT NOT NULL DEFAULT 2,
rec_amount_value BIGINT NOT NULL DEFAULT 0, rec_amount_value BIGINT NOT NULL DEFAULT 0,
rec_amount_scale SMALLINT NOT NULL DEFAULT 4, rec_amount_scale SMALLINT NOT NULL DEFAULT 4,
retention_code VARCHAR(40) NULL, retention_code VARCHAR(40) NULL,
retention_name VARCHAR(100) NULL, retention_name VARCHAR(100) NULL,
retention_group VARCHAR(40) NULL,
retention_calculation_behavior VARCHAR(40) NULL,
retention_percentage_value SMALLINT NULL, retention_percentage_value SMALLINT NULL,
retention_percentage_scale SMALLINT NOT NULL DEFAULT 2, retention_percentage_scale SMALLINT NOT NULL DEFAULT 2,
retention_amount_value BIGINT NOT NULL DEFAULT 0, retention_amount_value BIGINT NOT NULL DEFAULT 0,

View File

@ -8,6 +8,8 @@ import { Collection, Maybe, Result } from "@repo/rdx-utils";
import type { TaxDefinitionToTaxMapper } from "../../../common/mappers"; import type { TaxDefinitionToTaxMapper } from "../../../common/mappers";
import { ProformaItemTax } from "../../../../domain";
import { ProformaTaxResolver } from "./proforma-tax-resolver"; import { ProformaTaxResolver } from "./proforma-tax-resolver";
const required = <T>(result: { data: T }): T => result.data; const required = <T>(result: { data: T }): T => result.data;
@ -33,7 +35,7 @@ const buildResolver = (liveTaxes: Tax[]) => {
const finder = { const finder = {
findByCodes: async () => findByCodes: async () =>
Result.ok( Result.ok(
new Collection(liveTaxes.map((tax) => ({ code: tax.code } as TaxDefinitionPublicModel))) new Collection(liveTaxes.map((tax) => ({ code: tax.code }) as TaxDefinitionPublicModel))
), ),
} as unknown as ITaxDefinitionPublicFinder; } as unknown as ITaxDefinitionPublicFinder;
const taxMapper = { const taxMapper = {
@ -84,24 +86,27 @@ test("ProformaTaxResolver sustituye definiciones vivas y conserva las missing",
group: "retention", group: "retention",
calculationBehavior: "subtractive", calculationBehavior: "subtractive",
}); });
const snapshot = (tax: Tax) =>
required(ProformaItemTax.create({ code: tax.code, name: tax.name, percentage: tax.rate }));
const resolver = buildResolver([liveIva, liveRetention]); const resolver = buildResolver([liveIva, liveRetention]);
const result = await resolver.resolveExistingItemTaxes({ const result = await resolver.resolveExistingItemTaxes({
companyId: required(UniqueID.create("01900000-0000-7000-8000-000000000501")), companyId: required(UniqueID.create("01900000-0000-7000-8000-000000000501")),
itemsTaxes: new Collection([ itemsTaxes: new Collection([
{ {
iva: Maybe.some(persistedIva), iva: Maybe.some(snapshot(persistedIva)),
rec: Maybe.some(persistedRec), rec: Maybe.some(snapshot(persistedRec)),
retention: Maybe.some(persistedRetention), retention: Maybe.some(snapshot(persistedRetention)),
}, },
]), ]),
}); });
assert.equal(result.isSuccess, true); assert.equal(result.isSuccess, true);
const taxes = result.data.getAll()[0]; const taxes = result.data.getAll()[0];
assert.equal(taxes.iva.unwrap().equalsTo(liveIva), true); assert.equal(taxes.iva.unwrap().code, liveIva.code);
assert.equal(taxes.rec.unwrap().equalsTo(persistedRec), true); assert.equal(taxes.iva.unwrap().name, liveIva.name);
assert.equal(taxes.retention.unwrap().equalsTo(liveRetention), true); assert.equal(taxes.rec.unwrap().name, persistedRec.name);
assert.equal(taxes.iva.unwrap().value, 2200); assert.equal(taxes.retention.unwrap().name, liveRetention.name);
assert.notEqual(taxes.iva.unwrap().value, persistedIva.value); assert.equal(taxes.iva.unwrap().percentage.value, 2200);
assert.notEqual(taxes.iva.unwrap().percentage.value, persistedIva.value);
}); });

View File

@ -7,7 +7,11 @@ import type { Tax } from "@erp/core/api";
import type { UniqueID, UtcDate } from "@repo/rdx-ddd"; import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Collection, Maybe, Result } from "@repo/rdx-utils"; import { Collection, Maybe, Result } from "@repo/rdx-utils";
import { ProformaItemTaxes, type ProformaItemTaxesProps } from "../../../../domain"; import {
ProformaItemTax,
ProformaItemTaxes,
type ProformaItemTaxesProps,
} from "../../../../domain";
import type { TaxDefinitionToTaxMapper } from "../../../common/mappers"; import type { TaxDefinitionToTaxMapper } from "../../../common/mappers";
import type { ProformaItemTaxCodesInput } from "../../models"; import type { ProformaItemTaxCodesInput } from "../../models";
@ -170,13 +174,15 @@ export class ProformaTaxResolver {
return Result.fail(definitions.error); return Result.fail(definitions.error);
} }
const liveTaxesByCode = new Map<string, Tax>(); const liveTaxesByCode = new Map<string, ProformaItemTax>();
for (const definition of definitions.data.getAll()) { for (const definition of definitions.data.getAll()) {
const taxResult = this.deps.taxMapper.toTax(definition); const taxResult = this.deps.taxMapper.toTax(definition);
if (taxResult.isFailure) { if (taxResult.isFailure) {
return Result.fail(taxResult.error); return Result.fail(taxResult.error);
} }
liveTaxesByCode.set(taxResult.data.code, taxResult.data); const proformaTaxResult = this.toProformaItemTax(taxResult.data);
if (proformaTaxResult.isFailure) return Result.fail(proformaTaxResult.error);
liveTaxesByCode.set(proformaTaxResult.data.code, proformaTaxResult.data);
} }
return Result.ok( return Result.ok(
@ -201,7 +207,7 @@ export class ProformaTaxResolver {
private toMaybeTax( private toMaybeTax(
taxDefinitions: Collection<TaxDefinitionPublicModel>, taxDefinitions: Collection<TaxDefinitionPublicModel>,
code: Maybe<string> code: Maybe<string>
): Result<Maybe<Tax>, Error> { ): Result<Maybe<ProformaItemTax>, Error> {
if (code.isNone()) { if (code.isNone()) {
return Result.ok(Maybe.none()); return Result.ok(Maybe.none());
} }
@ -219,10 +225,19 @@ export class ProformaTaxResolver {
return Result.fail(tax.error); return Result.fail(tax.error);
} }
return Result.ok(Maybe.some(tax.data)); const proformaTax = this.toProformaItemTax(tax.data);
if (proformaTax.isFailure) return Result.fail(proformaTax.error);
return Result.ok(Maybe.some(proformaTax.data));
} }
private replaceWithLiveTax(tax: Maybe<Tax>, liveTaxesByCode: Map<string, Tax>): Maybe<Tax> { private toProformaItemTax(tax: Tax): Result<ProformaItemTax, Error> {
return ProformaItemTax.create({ code: tax.code, name: tax.name, percentage: tax.rate });
}
private replaceWithLiveTax(
tax: Maybe<ProformaItemTax>,
liveTaxesByCode: Map<string, ProformaItemTax>
): Maybe<ProformaItemTax> {
if (tax.isNone()) { if (tax.isNone()) {
return tax; return tax;
} }

View File

@ -1,18 +1,22 @@
import type { ICompanyPublicServices } from "@erp/companies/api"; import type { ICompanyPublicServices } from "@erp/companies/api";
import type { ICustomerPublicServices } from "@erp/customers/api";
import { DiscountPercentage } from "@erp/core/api"; import { DiscountPercentage } from "@erp/core/api";
import type { ICustomerPublicServices } from "@erp/customers/api";
import { UniqueID, UtcDate } from "@repo/rdx-ddd"; import { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { InvoiceStatus, ProformaRecipient, type RecurringProforma } from "../../../domain"; import { InvoiceStatus, ProformaRecipient, type RecurringProforma } from "../../../domain";
import type { ProformaCreateInputProps } from "../../proformas/models"; import type { ProformaCreateInputProps } from "../../proformas/models";
import type { IProformaCreator } from "../../proformas/services/proforma-creator";
import type { ProformaPaymentResolver } from "../../proformas/services/catalog-resolver/proforma-payment-resolver"; import type { ProformaPaymentResolver } from "../../proformas/services/catalog-resolver/proforma-payment-resolver";
import type { ProformaTaxConfigResolver } from "../../proformas/services/catalog-resolver/proforma-tax-config-resolver"; import type { ProformaTaxConfigResolver } from "../../proformas/services/catalog-resolver/proforma-tax-config-resolver";
import type { ProformaTaxResolver } from "../../proformas/services/catalog-resolver/proforma-tax-resolver"; import type { ProformaTaxResolver } from "../../proformas/services/catalog-resolver/proforma-tax-resolver";
import type { IProformaCreator } from "../../proformas/services/proforma-creator";
import type { ProformaSeriesValidator } from "../../proformas/services/proforma-series-validator.interface"; import type { ProformaSeriesValidator } from "../../proformas/services/proforma-series-validator.interface";
import type { ProformaTargetInvoiceSeriesValidator } from "../../proformas/services/proforma-target-invoice-series-validator.interface"; import type { ProformaTargetInvoiceSeriesValidator } from "../../proformas/services/proforma-target-invoice-series-validator.interface";
/**
* Genera una proforma a partir de una RecurringProforma"
*/
export class RecurringProformaMaterializer { export class RecurringProformaMaterializer {
public constructor( public constructor(
private readonly deps: { private readonly deps: {
@ -35,9 +39,11 @@ export class RecurringProformaMaterializer {
transaction: unknown; transaction: unknown;
}): Promise<Result<{ proformaId: UniqueID }, Error>> { }): Promise<Result<{ proformaId: UniqueID }, Error>> {
const customerId = params.recurringProforma.customerId; const customerId = params.recurringProforma.customerId;
if (customerId.isNone()) return Result.fail(new Error("Recurring proforma customer is required.")); if (customerId.isNone())
return Result.fail(new Error("Recurring proforma customer is required."));
const customerResult = await this.deps.customerServices.getCustomerById(customerId.unwrap(), { const customerResult = await this.deps.customerServices.getCustomerById(customerId.unwrap(), {
companyId: params.companyId, transaction: params.transaction, companyId: params.companyId,
transaction: params.transaction,
}); });
if (customerResult.isFailure) return Result.fail(customerResult.error); if (customerResult.isFailure) return Result.fail(customerResult.error);
const customer = customerResult.data; const customer = customerResult.data;
@ -46,38 +52,135 @@ export class RecurringProformaMaterializer {
const paymentTerm = await this.resolvePaymentTerm(params.recurringProforma, customer, params); const paymentTerm = await this.resolvePaymentTerm(params.recurringProforma, customer, params);
if (paymentTerm.isFailure) return Result.fail(paymentTerm.error); if (paymentTerm.isFailure) return Result.fail(paymentTerm.error);
const paymentMethod = params.recurringProforma.paymentMethodId.isSome() const paymentMethod = params.recurringProforma.paymentMethodId.isSome()
? params.recurringProforma.paymentMethodId : customer.paymentMethodId; ? params.recurringProforma.paymentMethodId
const paymentMethodValidation = await this.deps.paymentResolver.ensurePaymentMethodById({ companyId: params.companyId, id: paymentMethod, transaction: params.transaction }); : customer.paymentMethodId;
const paymentMethodValidation = await this.deps.paymentResolver.ensurePaymentMethodById({
companyId: params.companyId,
id: paymentMethod,
transaction: params.transaction,
});
if (paymentMethodValidation.isFailure) return Result.fail(paymentMethodValidation.error); if (paymentMethodValidation.isFailure) return Result.fail(paymentMethodValidation.error);
const languageCode = params.recurringProforma.language.isSome() ? params.recurringProforma.language.unwrap() : customer.languageCode; const languageCode = params.recurringProforma.language.isSome()
const currencyCode = params.recurringProforma.currency.isSome() ? params.recurringProforma.currency.unwrap() : customer.currencyCode; ? params.recurringProforma.language.unwrap()
: customer.languageCode;
const currencyCode = params.recurringProforma.currency.isSome()
? params.recurringProforma.currency.unwrap()
: customer.currencyCode;
const series = await this.validateSeries(params.recurringProforma, params); const series = await this.validateSeries(params.recurringProforma, params);
if (series.isFailure) return Result.fail(series.error); if (series.isFailure) return Result.fail(series.error);
const taxConfig = await this.deps.taxConfigResolver.resolve({ companyId: params.companyId, atDate: date.data, customer, transaction: params.transaction }); const taxConfig = await this.deps.taxConfigResolver.resolve({
companyId: params.companyId,
atDate: date.data,
customer,
transaction: params.transaction,
});
if (taxConfig.isFailure) return Result.fail(taxConfig.error); if (taxConfig.isFailure) return Result.fail(taxConfig.error);
const items: ProformaCreateInputProps["items"] = []; const items: ProformaCreateInputProps["items"] = [];
for (const item of params.recurringProforma.items.getAll()) { for (const item of params.recurringProforma.items.getAll()) {
const taxes = await this.deps.taxResolver.ensureItemTaxes({ companyId: params.companyId, atDate: date.data, transaction: params.transaction, taxCodes: { ivaCode: item.ivaCode, recCode: item.recCode, retentionCode: item.retentionCode } }); const taxes = await this.deps.taxResolver.ensureItemTaxes({
companyId: params.companyId,
atDate: date.data,
transaction: params.transaction,
taxCodes: {
ivaCode: item.ivaCode,
recCode: item.recCode,
retentionCode: item.retentionCode,
},
});
if (taxes.isFailure) return Result.fail(taxes.error); if (taxes.isFailure) return Result.fail(taxes.error);
items.push({ description: item.description, quantity: item.quantity, unitAmount: item.unitAmount, itemDiscountPercentage: item.itemDiscountPercentage, taxes: { ivaCode: item.ivaCode, recCode: item.recCode, retentionCode: item.retentionCode }, languageCode, currencyCode, globalDiscountPercentage: params.recurringProforma.globalDiscountPercentage.isSome() ? params.recurringProforma.globalDiscountPercentage.unwrap() : DiscountPercentage.zero() }); items.push({
description: item.description,
quantity: item.quantity,
unitAmount: item.unitAmount,
itemDiscountPercentage: item.itemDiscountPercentage,
taxes: { ivaCode: item.ivaCode, recCode: item.recCode, retentionCode: item.retentionCode },
languageCode,
currencyCode,
globalDiscountPercentage: params.recurringProforma.globalDiscountPercentage.isSome()
? params.recurringProforma.globalDiscountPercentage.unwrap()
: DiscountPercentage.zero(),
});
} }
const recipient = ProformaRecipient.create({ name: customer.name, tin: customer.tin, street: customer.address.street, street2: customer.address.street2, city: customer.address.city, postalCode: customer.address.postalCode, province: customer.address.province, country: customer.address.country }); const recipient = ProformaRecipient.create({
name: customer.name,
tin: customer.tin,
street: customer.address.street,
street2: customer.address.street2,
city: customer.address.city,
postalCode: customer.address.postalCode,
province: customer.address.province,
country: customer.address.countryCode,
});
if (recipient.isFailure) return Result.fail(recipient.error); if (recipient.isFailure) return Result.fail(recipient.error);
const status = InvoiceStatus.create("draft"); const status = InvoiceStatus.create("draft");
if (status.isFailure) return Result.fail(status.error); if (status.isFailure) return Result.fail(status.error);
const created = await this.deps.proformaCreator.create({ companyId: params.companyId, id: UniqueID.generateNewID(), transaction: params.transaction, props: { companyId: params.companyId, status: status.data, proformaSeriesCode: params.recurringProforma.proformaSeries, targetInvoiceSeriesCode: params.recurringProforma.issuedInvoiceSeries, proformaDate: date.data, operationDate: Maybe.some(date.data), customerId: customer.id, recipient: Maybe.some(recipient.data), reference: Maybe.none(), description: params.recurringProforma.description, notes: Maybe.none(), languageCode, currencyCode, linkedInvoiceId: Maybe.none(), paymentMethodId: paymentMethod, taxRegimeCode: customer.taxRegimeCode, taxConfig: taxConfig.data.getProps(), globalDiscountPercentage: params.recurringProforma.globalDiscountPercentage.isSome() ? params.recurringProforma.globalDiscountPercentage.unwrap() : DiscountPercentage.zero(), items } }); const created = await this.deps.proformaCreator.create({
return created.isFailure ? Result.fail(created.error) : Result.ok({ proformaId: created.data.id }); companyId: params.companyId,
id: UniqueID.generateNewID(),
transaction: params.transaction,
props: {
companyId: params.companyId,
status: status.data,
proformaSeriesCode: params.recurringProforma.proformaSeries,
targetInvoiceSeriesCode: params.recurringProforma.issuedInvoiceSeries,
proformaDate: date.data,
operationDate: Maybe.some(date.data),
customerId: customer.id,
recipient: Maybe.some(recipient.data),
reference: Maybe.none(),
description: params.recurringProforma.description,
notes: Maybe.none(),
languageCode,
currencyCode,
linkedInvoiceId: Maybe.none(),
paymentMethodId: paymentMethod,
taxRegimeCode: customer.taxRegimeCode,
taxConfig: taxConfig.data.getProps(),
globalDiscountPercentage: params.recurringProforma.globalDiscountPercentage.isSome()
? params.recurringProforma.globalDiscountPercentage.unwrap()
: DiscountPercentage.zero(),
items,
},
});
return created.isFailure
? Result.fail(created.error)
: Result.ok({ proformaId: created.data.id });
} }
private async validateSeries(recurring: RecurringProforma, params: { companyId: UniqueID; transaction: unknown }): Promise<Result<void, Error>> { private async validateSeries(
const proforma = await this.deps.proformaSeriesValidator.ensureValidProformaSeries({ companyId: params.companyId, proformaSeriesCode: recurring.proformaSeries, transaction: params.transaction }); recurring: RecurringProforma,
params: { companyId: UniqueID; transaction: unknown }
): Promise<Result<void, Error>> {
const proforma = await this.deps.proformaSeriesValidator.ensureValidProformaSeries({
companyId: params.companyId,
proformaSeriesCode: recurring.proformaSeries,
transaction: params.transaction,
});
if (proforma.isFailure) return Result.fail(proforma.error); if (proforma.isFailure) return Result.fail(proforma.error);
return this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({ companyId: params.companyId, targetInvoiceSeriesCode: recurring.issuedInvoiceSeries, transaction: params.transaction }); return this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
companyId: params.companyId,
targetInvoiceSeriesCode: recurring.issuedInvoiceSeries,
transaction: params.transaction,
});
} }
private async resolvePaymentTerm(recurring: RecurringProforma, customer: { paymentTermId: Maybe<UniqueID> }, params: { companyId: UniqueID; transaction: unknown }): Promise<Result<Maybe<UniqueID>, Error>> { private async resolvePaymentTerm(
const selected = recurring.paymentTermId.isSome() ? recurring.paymentTermId : customer.paymentTermId; recurring: RecurringProforma,
const validation = await this.deps.paymentResolver.ensurePaymentTermById({ companyId: params.companyId, id: selected, transaction: params.transaction }); customer: { paymentTermId: Maybe<UniqueID> },
params: { companyId: UniqueID; transaction: unknown }
): Promise<Result<Maybe<UniqueID>, Error>> {
const selected = recurring.paymentTermId.isSome()
? recurring.paymentTermId
: customer.paymentTermId;
const validation = await this.deps.paymentResolver.ensurePaymentTermById({
companyId: params.companyId,
id: selected,
transaction: params.transaction,
});
return validation.isFailure ? Result.fail(validation.error) : Result.ok(selected); return validation.isFailure ? Result.fail(validation.error) : Result.ok(selected);
} }
} }

View File

@ -1,4 +1,4 @@
import type { DiscountPercentage, Tax } from "@erp/core/api"; import type { DiscountPercentage } from "@erp/core/api";
import { import {
AggregateRoot, AggregateRoot,
type CurrencyCode, type CurrencyCode,
@ -41,6 +41,7 @@ import type { IProformaTaxTotals, ProformaCalculationContext } from "../services
import { canManuallyTransitionProformaStatus } from "../services"; import { canManuallyTransitionProformaStatus } from "../services";
import { import {
ProformaItemTaxes, ProformaItemTaxes,
type ProformaItemTax,
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
type ProformaRecipient, type ProformaRecipient,
type ProformaTaxConfig, type ProformaTaxConfig,
@ -408,14 +409,8 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
); );
} }
private sameTax(left: Tax, right: Tax): boolean { private sameTax(left: ProformaItemTax, right: ProformaItemTax): boolean {
return ( return left.equals(right);
left.code === right.code &&
left.name === right.name &&
left.rate.equals(right.rate) &&
left.group === right.group &&
left.calculationBehavior === right.calculationBehavior
);
} }
private sameMaybe<T>( private sameMaybe<T>(
@ -852,12 +847,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return Result.fail(taxesResult.error); return Result.fail(taxesResult.error);
} }
items.push( items.push(ProformaItem.rehydrate({ ...item.getProps(), taxes: taxesResult.data }, item.id));
ProformaItem.rehydrate(
{ ...item.getProps(), taxes: taxesResult.data },
item.id
)
);
} }
return Result.ok(Proforma.rehydrate(this.props, ProformaItems.create({ items }), this.id)); return Result.ok(Proforma.rehydrate(this.props, ProformaItems.create({ items }), this.id));

View File

@ -1,13 +1,14 @@
import { DiscountPercentage, type Tax, type TaxPercentage } from "@erp/core/api"; import { DiscountPercentage, type TaxPercentage } from "@erp/core/api";
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common"; import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common";
import type { ProformaCalculationContext } from "../../services"; import type { ProformaCalculationContext } from "../../services";
import { import {
type ProformaItemTax,
ProformaItemTaxes, ProformaItemTaxes,
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
} from "../../value-objects/proforma-item-taxes.vo"; } from "../../value-objects";
/** /**
* *
@ -166,7 +167,7 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return unitAmount.multiply(quantity); return unitAmount.multiply(quantity);
} }
public iva(): Maybe<Tax> { public iva(): Maybe<ProformaItemTax> {
return this.taxes.iva; return this.taxes.iva;
} }
@ -178,7 +179,7 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.taxes.iva.map((tax) => tax.percentage); return this.taxes.iva.map((tax) => tax.percentage);
} }
public rec(): Maybe<Tax> { public rec(): Maybe<ProformaItemTax> {
return this.taxes.rec; return this.taxes.rec;
} }
@ -190,7 +191,7 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.taxes.rec.map((tax) => tax.percentage); return this.taxes.rec.map((tax) => tax.percentage);
} }
public retention(): Maybe<Tax> { public retention(): Maybe<ProformaItemTax> {
return this.taxes.retention; return this.taxes.retention;
} }

View File

@ -1,8 +1,9 @@
import type { Tax, TaxPercentage } from "@erp/core/api"; import type { TaxPercentage } from "@erp/core/api";
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { ItemAmount } from "../../common"; import { ItemAmount } from "../../common";
import type { IProformaItems } from "../entities"; import type { IProformaItems } from "../entities";
import type { ProformaItemTax } from "../value-objects";
import type { ProformaCalculationContext } from "./proforma-items-totals-calculator"; import type { ProformaCalculationContext } from "./proforma-items-totals-calculator";
@ -97,7 +98,7 @@ export function proformaComputeTaxGroups(
function ensureTaxNameMatches( function ensureTaxNameMatches(
code: Maybe<string>, code: Maybe<string>,
name: Maybe<string>, name: Maybe<string>,
tax: Maybe<Tax>, tax: Maybe<ProformaItemTax>,
kind: string kind: string
): void { ): void {
if (code.isNone() || tax.isNone()) return; if (code.isNone() || tax.isNone()) return;
@ -109,7 +110,7 @@ function ensureTaxNameMatches(
function ensureTaxPercentageMatches( function ensureTaxPercentageMatches(
code: Maybe<string>, code: Maybe<string>,
percentage: Maybe<TaxPercentage>, percentage: Maybe<TaxPercentage>,
tax: Maybe<Tax>, tax: Maybe<ProformaItemTax>,
kind: string kind: string
): void { ): void {
if (code.isNone() || tax.isNone()) return; if (code.isNone() || tax.isNone()) return;
@ -123,7 +124,11 @@ function ensureTaxPercentageMatches(
} }
} }
function buildTaxGroupKey(iva: Maybe<Tax>, rec: Maybe<Tax>, retention: Maybe<Tax>): string { function buildTaxGroupKey(
iva: Maybe<ProformaItemTax>,
rec: Maybe<ProformaItemTax>,
retention: Maybe<ProformaItemTax>
): string {
const ivaPart = iva.isSome() ? `${iva.unwrap().code}` : "NULL"; const ivaPart = iva.isSome() ? `${iva.unwrap().code}` : "NULL";
const recPart = rec.isSome() ? `${rec.unwrap().code}` : "NULL"; const recPart = rec.isSome() ? `${rec.unwrap().code}` : "NULL";
const retentionPart = retention.isSome() ? `${retention.unwrap().code}` : "NULL"; const retentionPart = retention.isSome() ? `${retention.unwrap().code}` : "NULL";

View File

@ -1,11 +1,12 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { Tax, TaxPercentage } from "@erp/core/api"; import { TaxPercentage } from "@erp/core/api";
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { ItemAmount, ItemQuantity } from "../../common"; import { ItemAmount, ItemQuantity } from "../../common";
import type { IProformaItemCreateProps } from "../entities"; import type { IProformaItemCreateProps } from "../entities";
import { ProformaItemTax } from "../value-objects";
import { createProformaTestFixture } from "../testing/proforma-test-fixture"; import { createProformaTestFixture } from "../testing/proforma-test-fixture";
const required = <T>(result: { data: T }): T => result.data; const required = <T>(result: { data: T }): T => result.data;
@ -18,12 +19,10 @@ const createTax = (params: {
calculationBehavior: "additive" | "subtractive"; calculationBehavior: "additive" | "subtractive";
}) => }) =>
required( required(
Tax.create({ ProformaItemTax.create({
code: params.code, code: params.code,
name: params.name, name: params.name,
rate: required(TaxPercentage.create({ value: params.value })), percentage: required(TaxPercentage.create({ value: params.value })),
group: params.group,
calculationBehavior: params.calculationBehavior,
}) })
); );

View File

@ -1,3 +1,4 @@
export * from "./proforma-item-tax.vo";
export * from "./proforma-item-taxes.vo"; export * from "./proforma-item-taxes.vo";
export * from "./proforma-recipient.vo"; export * from "./proforma-recipient.vo";
export * from "./proforma-tax-config.vo"; export * from "./proforma-tax-config.vo";

View File

@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import test from "node:test";
import { TaxPercentage } from "@erp/core/api";
import { ProformaItemTax } from "./proforma-item-tax.vo";
const required = <T>(result: { data: T }): T => result.data;
test("ProformaItemTax compara code, name y percentage", () => {
const percentage = required(TaxPercentage.create({ value: 2100 }));
const tax = required(ProformaItemTax.create({ code: "iva_21", name: "IVA", percentage }));
const equal = required(ProformaItemTax.create({ code: "iva_21", name: "IVA", percentage }));
const renamed = required(
ProformaItemTax.create({ code: "iva_21", name: "IVA general", percentage })
);
assert.equal(tax.equalsTo(equal), true);
assert.equal(tax.equalsTo(renamed), false);
});

View File

@ -0,0 +1,39 @@
import type { TaxPercentage } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
export type ProformaItemTaxProps = {
code: string;
name: string;
percentage: TaxPercentage;
};
export class ProformaItemTax extends ValueObject<ProformaItemTaxProps> {
static create(props: ProformaItemTaxProps): Result<ProformaItemTax, Error> {
return Result.ok(new ProformaItemTax(props));
}
public equalsTo(other: ProformaItemTax): boolean {
return (
this.code === other.code &&
this.name === other.name &&
this.percentage.equals(other.percentage)
);
}
public get code(): string {
return this.props.code;
}
public get name(): string {
return this.props.name;
}
public get percentage(): TaxPercentage {
return this.props.percentage;
}
public getProps(): ProformaItemTaxProps {
return this.props;
}
public toPrimitive(): ProformaItemTaxProps {
return this.props;
}
}

View File

@ -1,19 +1,20 @@
import type { Tax } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd"; import { ValueObject } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { ItemAmount } from "../../common/value-objects"; import { ItemAmount } from "../../common/value-objects";
import type { ProformaItemTax } from "./proforma-item-tax.vo";
export type ProformaItemTaxesProps = { export type ProformaItemTaxesProps = {
iva: Maybe<Tax>; // si existe iva: Maybe<ProformaItemTax>; // si existe
rec: Maybe<Tax>; // si existe rec: Maybe<ProformaItemTax>; // si existe
retention: Maybe<Tax>; // si existe retention: Maybe<ProformaItemTax>; // si existe
}; };
export interface IProformaItemTaxes { export interface IProformaItemTaxes {
iva: Maybe<Tax>; // si existe iva: Maybe<ProformaItemTax>; // si existe
rec: Maybe<Tax>; // si existe rec: Maybe<ProformaItemTax>; // si existe
retention: Maybe<Tax>; // si existe retention: Maybe<ProformaItemTax>; // si existe
toKey(): string; // Clave para representar un trío. toKey(): string; // Clave para representar un trío.
@ -83,15 +84,15 @@ export class ProformaItemTaxes
return { ivaAmount, recAmount, retentionAmount }; return { ivaAmount, recAmount, retentionAmount };
} }
get iva(): Maybe<Tax> { get iva(): Maybe<ProformaItemTax> {
return this.props.iva; return this.props.iva;
} }
get rec(): Maybe<Tax> { get rec(): Maybe<ProformaItemTax> {
return this.props.rec; return this.props.rec;
} }
get retention(): Maybe<Tax> { get retention(): Maybe<ProformaItemTax> {
return this.props.retention; return this.props.retention;
} }

View File

@ -110,10 +110,7 @@ export class RecurrenceSchedule extends ValueObject<RecurrenceScheduleProps> {
const occurrenceDate = this.calculateOccurrenceAt(index); const occurrenceDate = this.calculateOccurrenceAt(index);
if ( if (this.props.endDate !== null && occurrenceDate.getTime() > this.props.endDate.getTime()) {
this.props.endDate !== null &&
occurrenceDate.getTime() > this.props.endDate.getTime()
) {
return Result.ok(null); return Result.ok(null);
} }
@ -130,6 +127,10 @@ export class RecurrenceSchedule extends ValueObject<RecurrenceScheduleProps> {
}; };
} }
public toPrimitive(): RecurrenceScheduleProps {
return this.getProps();
}
private calculateOccurrenceAt(index: number): Date { private calculateOccurrenceAt(index: number): Date {
switch (this.props.unit) { switch (this.props.unit) {
case "week": case "week":
@ -187,9 +188,7 @@ export class RecurrenceSchedule extends ValueObject<RecurrenceScheduleProps> {
} }
private static normalizeDate(value: Date): Date { private static normalizeDate(value: Date): Date {
return new Date( return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate())
);
} }
private static cloneDate(value: Date): Date { private static cloneDate(value: Date): Date {

View File

@ -1,6 +1,6 @@
import { type DiscountPercentage } from "@erp/core/api"; import type { DiscountPercentage } from "@erp/core/api";
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import type { ItemAmount, ItemDescription, ItemQuantity } from "../common"; import type { ItemAmount, ItemDescription, ItemQuantity } from "../common";
@ -14,7 +14,10 @@ export interface RecurringProformaItemCreateProps {
retentionCode?: Maybe<string>; retentionCode?: Maybe<string>;
} }
type RecurringProformaItemInternalProps = Omit<RecurringProformaItemCreateProps, "ivaCode" | "recCode" | "retentionCode"> & { type RecurringProformaItemInternalProps = Omit<
RecurringProformaItemCreateProps,
"ivaCode" | "recCode" | "retentionCode"
> & {
ivaCode: Maybe<string>; ivaCode: Maybe<string>;
recCode: Maybe<string>; recCode: Maybe<string>;
retentionCode: Maybe<string>; retentionCode: Maybe<string>;
@ -25,12 +28,17 @@ export class RecurringProformaItem extends DomainEntity<RecurringProformaItemInt
props: RecurringProformaItemCreateProps, props: RecurringProformaItemCreateProps,
id?: UniqueID id?: UniqueID
): Result<RecurringProformaItem, Error> { ): Result<RecurringProformaItem, Error> {
return Result.ok(new RecurringProformaItem({ return Result.ok(
...props, new RecurringProformaItem(
ivaCode: props.ivaCode ?? Maybe.none(), {
recCode: props.recCode ?? Maybe.none(), ...props,
retentionCode: props.retentionCode ?? Maybe.none(), ivaCode: props.ivaCode ?? Maybe.none(),
}, id)); recCode: props.recCode ?? Maybe.none(),
retentionCode: props.retentionCode ?? Maybe.none(),
},
id
)
);
} }
public static rehydrate( public static rehydrate(
@ -60,11 +68,17 @@ export class RecurringProformaItem extends DomainEntity<RecurringProformaItemInt
return this.props.itemDiscountPercentage; return this.props.itemDiscountPercentage;
} }
public get ivaCode(): Maybe<string> { return this.props.ivaCode; } public get ivaCode(): Maybe<string> {
return this.props.ivaCode;
}
public get recCode(): Maybe<string> { return this.props.recCode; } public get recCode(): Maybe<string> {
return this.props.recCode;
}
public get retentionCode(): Maybe<string> { return this.props.retentionCode; } public get retentionCode(): Maybe<string> {
return this.props.retentionCode;
}
public getProps(): RecurringProformaItemCreateProps { public getProps(): RecurringProformaItemCreateProps {
return { return {

View File

@ -43,24 +43,18 @@ export class ProformaItemModel extends Model<
declare taxable_amount_scale: number; declare taxable_amount_scale: number;
declare iva_code: CreationOptional<string | null>; declare iva_code: CreationOptional<string | null>;
declare iva_name: CreationOptional<string | null>; declare iva_name: CreationOptional<string | null>;
declare iva_group: CreationOptional<string | null>;
declare iva_calculation_behavior: CreationOptional<string | null>;
declare iva_percentage_value: CreationOptional<number | null>; declare iva_percentage_value: CreationOptional<number | null>;
declare iva_percentage_scale: number; declare iva_percentage_scale: number;
declare iva_amount_value: number; declare iva_amount_value: number;
declare iva_amount_scale: number; declare iva_amount_scale: number;
declare rec_code: CreationOptional<string | null>; declare rec_code: CreationOptional<string | null>;
declare rec_name: CreationOptional<string | null>; declare rec_name: CreationOptional<string | null>;
declare rec_group: CreationOptional<string | null>;
declare rec_calculation_behavior: CreationOptional<string | null>;
declare rec_percentage_value: CreationOptional<number | null>; declare rec_percentage_value: CreationOptional<number | null>;
declare rec_percentage_scale: number; declare rec_percentage_scale: number;
declare rec_amount_value: number; declare rec_amount_value: number;
declare rec_amount_scale: number; declare rec_amount_scale: number;
declare retention_code: CreationOptional<string | null>; declare retention_code: CreationOptional<string | null>;
declare retention_name: CreationOptional<string | null>; declare retention_name: CreationOptional<string | null>;
declare retention_group: CreationOptional<string | null>;
declare retention_calculation_behavior: CreationOptional<string | null>;
declare retention_percentage_value: CreationOptional<number | null>; declare retention_percentage_value: CreationOptional<number | null>;
declare retention_percentage_scale: number; declare retention_percentage_scale: number;
declare retention_amount_value: number; declare retention_amount_value: number;
@ -216,9 +210,11 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
iva_name: { type: DataTypes.STRING(100), allowNull: true, defaultValue: null }, iva_name: {
iva_group: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, type: DataTypes.STRING(100),
iva_calculation_behavior: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, allowNull: true,
defaultValue: null,
},
iva_percentage_value: { iva_percentage_value: {
type: DataTypes.SMALLINT, type: DataTypes.SMALLINT,
allowNull: true, allowNull: true,
@ -244,9 +240,11 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
rec_name: { type: DataTypes.STRING(100), allowNull: true, defaultValue: null }, rec_name: {
rec_group: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, type: DataTypes.STRING(100),
rec_calculation_behavior: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, allowNull: true,
defaultValue: null,
},
rec_percentage_value: { rec_percentage_value: {
type: DataTypes.SMALLINT, type: DataTypes.SMALLINT,
allowNull: true, allowNull: true,
@ -272,9 +270,11 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
retention_name: { type: DataTypes.STRING(100), allowNull: true, defaultValue: null }, retention_name: {
retention_group: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, type: DataTypes.STRING(100),
retention_calculation_behavior: { type: DataTypes.STRING(40), allowNull: true, defaultValue: null }, allowNull: true,
defaultValue: null,
},
retention_percentage_value: { retention_percentage_value: {
type: DataTypes.SMALLINT, type: DataTypes.SMALLINT,
allowNull: true, allowNull: true,

View File

@ -1,11 +1,11 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { Tax, TaxPercentage } from "@erp/core/api"; import { TaxPercentage } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { type Proforma, ProformaItem } from "../../../../../../domain"; import { type Proforma, ProformaItem, ProformaItemTax } from "../../../../../../domain";
import type { ProformaItemModel } from "../../../../../common"; import type { ProformaItemModel } from "../../../../../common";
import { SequelizeProformaV2ItemDomainMapper } from "./sequelize-proforma-v2-item-domain.mapper"; import { SequelizeProformaV2ItemDomainMapper } from "./sequelize-proforma-v2-item-domain.mapper";
@ -27,12 +27,10 @@ const createTax = (params: {
calculationBehavior: "additive" | "subtractive"; calculationBehavior: "additive" | "subtractive";
}) => }) =>
required( required(
Tax.create({ ProformaItemTax.create({
code: params.code, code: params.code,
name: params.name, name: params.name,
rate: required(TaxPercentage.create({ value: params.value })), percentage: required(TaxPercentage.create({ value: params.value })),
group: params.group,
calculationBehavior: params.calculationBehavior,
}) })
); );
@ -47,9 +45,7 @@ const roundTrip = (taxes: Parameters<typeof ProformaItem.create>[0]["taxes"]) =>
}) })
); );
const mapper = new SequelizeProformaV2ItemDomainMapper(); const mapper = new SequelizeProformaV2ItemDomainMapper();
const persistence = required( const persistence = required(mapper.mapToPersistence(source, { index: 0, parent, errors: [] }));
mapper.mapToPersistence(source, { index: 0, parent, errors: [] })
);
const rehydrated = required( const rehydrated = required(
mapper.mapToDomain(persistence as unknown as ProformaItemModel, { mapper.mapToDomain(persistence as unknown as ProformaItemModel, {
index: 0, index: 0,
@ -77,13 +73,8 @@ test("SequelizeProformaV2ItemDomainMapper conserva IVA completo sin fabricar nam
assert.equal(persistence.iva_code, "iva_21"); assert.equal(persistence.iva_code, "iva_21");
assert.equal(persistence.iva_name, "IVA general real"); assert.equal(persistence.iva_name, "IVA general real");
assert.equal(persistence.iva_group, "iva");
assert.equal(persistence.iva_calculation_behavior, "additive");
assert.equal(rehydrated.taxes.iva.unwrap().name, "IVA general real"); assert.equal(rehydrated.taxes.iva.unwrap().name, "IVA general real");
assert.notEqual( assert.notEqual(rehydrated.taxes.iva.unwrap().name, rehydrated.taxes.iva.unwrap().code);
rehydrated.taxes.iva.unwrap().name,
rehydrated.taxes.iva.unwrap().code
);
}); });
test("SequelizeProformaV2ItemDomainMapper conserva REC completo", () => { test("SequelizeProformaV2ItemDomainMapper conserva REC completo", () => {
@ -101,8 +92,6 @@ test("SequelizeProformaV2ItemDomainMapper conserva REC completo", () => {
}); });
assert.equal(persistence.rec_name, rec.name); assert.equal(persistence.rec_name, rec.name);
assert.equal(persistence.rec_group, rec.group);
assert.equal(persistence.rec_calculation_behavior, rec.calculationBehavior);
assert.equal(rehydrated.taxes.rec.unwrap().equalsTo(rec), true); assert.equal(rehydrated.taxes.rec.unwrap().equalsTo(rec), true);
}); });
@ -121,8 +110,6 @@ test("SequelizeProformaV2ItemDomainMapper conserva retención completa", () => {
}); });
assert.equal(persistence.retention_name, retention.name); assert.equal(persistence.retention_name, retention.name);
assert.equal(persistence.retention_group, retention.group);
assert.equal(persistence.retention_calculation_behavior, retention.calculationBehavior);
assert.equal(rehydrated.taxes.retention.unwrap().equalsTo(retention), true); assert.equal(rehydrated.taxes.retention.unwrap().equalsTo(retention), true);
}); });

View File

@ -2,9 +2,6 @@ import {
DiscountPercentage, DiscountPercentage,
type MapperParamsType, type MapperParamsType,
SequelizeDomainMapper, SequelizeDomainMapper,
Tax,
type TaxCalculationBehavior,
type TaxGroup,
TaxPercentage, TaxPercentage,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
@ -25,6 +22,7 @@ import {
ItemQuantity, ItemQuantity,
type Proforma, type Proforma,
ProformaItem, ProformaItem,
ProformaItemTax,
ProformaItemTaxes, ProformaItemTaxes,
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
} from "../../../../../../domain"; } from "../../../../../../domain";
@ -90,8 +88,6 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
this.mapTaxToDomain({ this.mapTaxToDomain({
code: raw.iva_code, code: raw.iva_code,
name: raw.iva_name, name: raw.iva_name,
group: raw.iva_group,
calculationBehavior: raw.iva_calculation_behavior,
percentageValue: raw.iva_percentage_value, percentageValue: raw.iva_percentage_value,
fieldPath: `items[${index}].iva`, fieldPath: `items[${index}].iva`,
}), }),
@ -103,8 +99,6 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
this.mapTaxToDomain({ this.mapTaxToDomain({
code: raw.rec_code, code: raw.rec_code,
name: raw.rec_name, name: raw.rec_name,
group: raw.rec_group,
calculationBehavior: raw.rec_calculation_behavior,
percentageValue: raw.rec_percentage_value, percentageValue: raw.rec_percentage_value,
fieldPath: `items[${index}].rec`, fieldPath: `items[${index}].rec`,
}), }),
@ -116,8 +110,6 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
this.mapTaxToDomain({ this.mapTaxToDomain({
code: raw.retention_code, code: raw.retention_code,
name: raw.retention_name, name: raw.retention_name,
group: raw.retention_group,
calculationBehavior: raw.retention_calculation_behavior,
percentageValue: raw.retention_percentage_value, percentageValue: raw.retention_percentage_value,
fieldPath: `items[${index}].retention`, fieldPath: `items[${index}].retention`,
}), }),
@ -141,17 +133,10 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
code: string | null; code: string | null;
name: string | null; name: string | null;
percentageValue: number | null; percentageValue: number | null;
group: string | null;
calculationBehavior: string | null;
fieldPath: string; fieldPath: string;
}): Result<Maybe<Tax>, Error> { }): Result<Maybe<ProformaItemTax>, Error> {
if (params.code === null || params.code.trim() === "") { if (params.code === null || params.code.trim() === "") {
if ( if (params.name !== null || params.percentageValue !== null) {
params.name !== null ||
params.percentageValue !== null ||
params.group !== null ||
params.calculationBehavior !== null
) {
return Result.fail( return Result.fail(
new Error(`${params.fieldPath} metadata must be absent when tax code is absent`) new Error(`${params.fieldPath} metadata must be absent when tax code is absent`)
); );
@ -165,8 +150,10 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
new Error(`${params.fieldPath}.percentage_value is required when tax code is present`) new Error(`${params.fieldPath}.percentage_value is required when tax code is present`)
); );
} }
if (!params.name || !params.group || !params.calculationBehavior) { if (!params.name) {
return Result.fail(new Error(`${params.fieldPath} metadata is required when tax code is present`)); return Result.fail(
new Error(`${params.fieldPath}.name is required when tax code is present`)
);
} }
const percentageResult = TaxPercentage.create({ const percentageResult = TaxPercentage.create({
@ -177,12 +164,10 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
return Result.fail(percentageResult.error); return Result.fail(percentageResult.error);
} }
const taxResult = Tax.create({ const taxResult = ProformaItemTax.create({
code: params.code, code: params.code,
name: params.name, name: params.name,
rate: percentageResult.data, percentage: percentageResult.data,
group: params.group as TaxGroup,
calculationBehavior: params.calculationBehavior as TaxCalculationBehavior,
}); });
if (taxResult.isFailure) { if (taxResult.isFailure) {
@ -192,7 +177,10 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
return Result.ok(Maybe.some(taxResult.data)); return Result.ok(Maybe.some(taxResult.data));
} }
public mapToDomain(raw: ProformaItemModel, params?: MapperParamsType): Result<ProformaItem, Error> { public mapToDomain(
raw: ProformaItemModel,
params?: MapperParamsType
): Result<ProformaItem, Error> {
const { errors } = params as { const { errors } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
@ -282,42 +270,29 @@ export class SequelizeProformaV2ItemDomainMapper extends SequelizeDomainMapper<
taxable_amount_scale: allAmounts.taxableAmount.scale, taxable_amount_scale: allAmounts.taxableAmount.scale,
iva_code: maybeToNullable(source.taxes.iva, (value) => value.code), iva_code: maybeToNullable(source.taxes.iva, (value) => value.code),
iva_name: maybeToNullable(source.taxes.iva, (value) => value.name), iva_name: maybeToNullable(source.taxes.iva, (value) => value.name),
iva_group: maybeToNullable(source.taxes.iva, (value) => value.group),
iva_calculation_behavior: maybeToNullable(
source.taxes.iva,
(value) => value.calculationBehavior
),
iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value), iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value),
iva_percentage_scale: iva_percentage_scale:
maybeToNullable(source.taxes.iva, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, maybeToNullable(source.taxes.iva, (value) => value.percentage.scale) ??
TaxPercentage.DEFAULT_SCALE,
iva_amount_value: allAmounts.ivaAmount.value, iva_amount_value: allAmounts.ivaAmount.value,
iva_amount_scale: allAmounts.ivaAmount.scale, iva_amount_scale: allAmounts.ivaAmount.scale,
rec_code: maybeToNullable(source.taxes.rec, (value) => value.code), rec_code: maybeToNullable(source.taxes.rec, (value) => value.code),
rec_name: maybeToNullable(source.taxes.rec, (value) => value.name), rec_name: maybeToNullable(source.taxes.rec, (value) => value.name),
rec_group: maybeToNullable(source.taxes.rec, (value) => value.group),
rec_calculation_behavior: maybeToNullable(
source.taxes.rec,
(value) => value.calculationBehavior
),
rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value), rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value),
rec_percentage_scale: rec_percentage_scale:
maybeToNullable(source.taxes.rec, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, maybeToNullable(source.taxes.rec, (value) => value.percentage.scale) ??
TaxPercentage.DEFAULT_SCALE,
rec_amount_value: allAmounts.recAmount.value, rec_amount_value: allAmounts.recAmount.value,
rec_amount_scale: allAmounts.recAmount.scale, rec_amount_scale: allAmounts.recAmount.scale,
retention_code: maybeToNullable(source.taxes.retention, (value) => value.code), retention_code: maybeToNullable(source.taxes.retention, (value) => value.code),
retention_name: maybeToNullable(source.taxes.retention, (value) => value.name), retention_name: maybeToNullable(source.taxes.retention, (value) => value.name),
retention_group: maybeToNullable(source.taxes.retention, (value) => value.group),
retention_calculation_behavior: maybeToNullable(
source.taxes.retention,
(value) => value.calculationBehavior
),
retention_percentage_value: maybeToNullable( retention_percentage_value: maybeToNullable(
source.taxes.retention, source.taxes.retention,
(value) => value.percentage.value (value) => value.percentage.value
), ),
retention_percentage_scale: retention_percentage_scale:
maybeToNullable(source.taxes.retention, (value) => value.percentage.scale) ?? maybeToNullable(source.taxes.retention, (value) => value.percentage.scale) ??
Tax.DEFAULT_SCALE, TaxPercentage.DEFAULT_SCALE,
retention_amount_value: allAmounts.retentionAmount.value, retention_amount_value: allAmounts.retentionAmount.value,
retention_amount_scale: allAmounts.retentionAmount.scale, retention_amount_scale: allAmounts.retentionAmount.scale,
taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_value: allAmounts.taxesAmount.value,

View File

@ -33,6 +33,14 @@ export class UtcDate extends ValueObject<UtcDateProps> {
return UtcDate.create(dateString); return UtcDate.create(dateString);
} }
static createFromDate(date: Date): Result<UtcDate, Error> {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDate()).padStart(2, "0");
return UtcDate.create(`${year}-${month}-${day}`);
}
static today(): UtcDate { static today(): UtcDate {
const now = new Date(); const now = new Date();