This commit is contained in:
David Arranz 2026-02-21 21:46:27 +01:00
parent e9824ecf80
commit cc38faed94
46 changed files with 678 additions and 1076 deletions

View File

@ -1,4 +1,4 @@
import { Tax } from "../tax"; import { Tax } from "../tax.vo";
describe("Tax Value Object", () => { describe("Tax Value Object", () => {
describe("Creación", () => { describe("Creación", () => {
@ -244,7 +244,7 @@ describe("Tax Value Object", () => {
if (result.isSuccess) { if (result.isSuccess) {
const tax = result.value; const tax = result.value;
const json = tax.toJSON(); const json = tax.toJSON();
expect(json).toEqual({ expect(json).toEqual({
value: 2100, value: 2100,
scale: 2, scale: 2,
@ -285,7 +285,7 @@ describe("Tax Value Object", () => {
if (result.isSuccess) { if (result.isSuccess) {
const tax = result.value; const tax = result.value;
const props = tax.getProps(); const props = tax.getProps();
// Intentar modificar las propiedades debe fallar // Intentar modificar las propiedades debe fallar
expect(() => { expect(() => {
(props as any).value = 3000; (props as any).value = 3000;

View File

@ -0,0 +1,19 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type DiscountPercentageProps = Pick<PercentageProps, "value">;
export class DiscountPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: DiscountPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: DiscountPercentage.DEFAULT_SCALE,
});
}
static zero() {
return DiscountPercentage.create({ value: 0 }).data;
}
}

View File

@ -1 +1,3 @@
export * from "./tax"; export * from "./discount-percentage.vo";
export * from "./tax.vo";
export * from "./tax-percentage.vo";

View File

@ -0,0 +1,19 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type TaxPercentageProps = Pick<PercentageProps, "value">;
export class TaxPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: TaxPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: TaxPercentage.DEFAULT_SCALE,
});
}
static zero() {
return TaxPercentage.create({ value: 0 }).data;
}
}

View File

@ -1,20 +1,21 @@
import type { TaxCatalogProvider } from "@erp/core"; import type { TaxCatalogProvider } from "@erp/core";
import { Percentage, ValueObject } from "@repo/rdx-ddd"; import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";
const DEFAULT_SCALE = 2; import { TaxPercentage } from "./tax-percentage.vo";
const DEFAULT_MIN_VALUE = 0;
const DEFAULT_MAX_VALUE = 100;
const DEFAULT_MIN_SCALE = 0; const DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
const DEFAULT_MAX_SCALE = 4; const DEFAULT_MIN_VALUE = TaxPercentage.MIN_VALUE;
const DEFAULT_MAX_VALUE = TaxPercentage.MAX_VALUE;
const DEFAULT_MIN_SCALE = TaxPercentage.MIN_SCALE;
const DEFAULT_MAX_SCALE = TaxPercentage.MAX_SCALE;
export interface TaxProps { export interface TaxProps {
code: string; // iva_21 code: string; // iva_21
name: string; // 21% IVA name: string; // 21% IVA
value: number; // 2100 value: number; // 2100
scale: number; // 2
} }
export class Tax extends ValueObject<TaxProps> { export class Tax extends ValueObject<TaxProps> {
@ -26,7 +27,7 @@ export class Tax extends ValueObject<TaxProps> {
private static CODE_REGEX = /^[a-z0-9_:-]+$/; private static CODE_REGEX = /^[a-z0-9_:-]+$/;
private _percentage!: Percentage; private _percentage!: TaxPercentage;
protected static validate(values: TaxProps) { protected static validate(values: TaxProps) {
const schema = z.object({ const schema = z.object({
@ -55,19 +56,14 @@ export class Tax extends ValueObject<TaxProps> {
} }
static create(props: TaxProps): Result<Tax> { static create(props: TaxProps): Result<Tax> {
const { value, scale = Tax.DEFAULT_SCALE, name, code } = props; const { value, name, code } = props;
const validationResult = Tax.validate({ value, scale, name, code }); const validationResult = Tax.validate({ value, name, code });
if (!validationResult.success) { if (!validationResult.success) {
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", "))); return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
} }
const realValue = value / 10 ** scale; return Result.ok(new Tax({ value, name, code }));
if (realValue > Tax.MAX_VALUE) {
return Result.fail(new Error("La tasa de impuesto no puede ser mayor a 100%."));
}
return Result.ok(new Tax({ value, scale, name, code }));
} }
/** /**
@ -101,7 +97,6 @@ export class Tax extends ValueObject<TaxProps> {
// Delegamos en create para reusar validación y límites // Delegamos en create para reusar validación y límites
return Tax.create({ return Tax.create({
value: Number(item.value), value: Number(item.value),
scale: Number(item.scale) ?? Tax.DEFAULT_SCALE,
name: item.name, name: item.name,
code: item.code, // guardamos el code tal cual viene del catálogo code: item.code, // guardamos el code tal cual viene del catálogo
}); });
@ -109,9 +104,8 @@ export class Tax extends ValueObject<TaxProps> {
protected constructor(props: TaxProps) { protected constructor(props: TaxProps) {
super(props); super(props);
this._percentage = Percentage.create({ this._percentage = TaxPercentage.create({
value: this.props.value, value: this.props.value,
scale: this.props.scale,
}).data; }).data;
} }
@ -119,7 +113,7 @@ export class Tax extends ValueObject<TaxProps> {
return this.props.value; return this.props.value;
} }
get scale(): number { get scale(): number {
return this.props.scale; return Tax.DEFAULT_SCALE;
} }
get name(): string { get name(): string {
return this.props.name; return this.props.name;
@ -128,7 +122,7 @@ export class Tax extends ValueObject<TaxProps> {
return this.props.code; return this.props.code;
} }
get percentage(): Percentage { get percentage(): TaxPercentage {
return this._percentage; return this._percentage;
} }

View File

@ -1,9 +0,0 @@
import { Collection } from "@repo/rdx-utils";
import type { Tax } from "./tax";
export class Taxes extends Collection<Tax> {
public static create<T extends Taxes>(this: new (items: Tax[]) => T, items: Tax[]): T {
return new Taxes(items);
}
}

View File

@ -1,7 +1,7 @@
export * from "./application-models"; export * from "./application-models";
export * from "./di"; export * from "./di";
export * from "./dtos"; export * from "./dtos";
//export * from "./mappers"; export * from "./mappers";
export * from "./repositories"; export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

@ -1,2 +1 @@
export * from "./invoice-payment-method"; export * from "./invoice-payment-method";
export * from "./invoice-taxes";

View File

@ -1,2 +0,0 @@
export * from "./invoice-tax";
export * from "./invoice-taxes";

View File

@ -1,38 +0,0 @@
import type { Tax } from "@erp/core/api";
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../value-objects/invoice-amount.vo";
export interface InvoiceTaxProps {
tax: Tax;
taxesAmount: InvoiceAmount;
}
export class InvoiceTax extends DomainEntity<InvoiceTaxProps> {
static create(props: InvoiceTaxProps, id?: UniqueID): Result<InvoiceTax, Error> {
const invoiceTax = new InvoiceTax(props, id);
// Reglas de negocio / validaciones
// ...
// ...
return Result.ok(invoiceTax);
}
public get tax(): Tax {
return this.props.tax;
}
getProps(): InvoiceTaxProps {
return this.props;
}
toPrimitive() {
return this.getProps();
}
public getTaxAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
return taxableAmount.percentage(this.tax.percentage);
}
}

View File

@ -1,40 +0,0 @@
import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount, type InvoiceTaxGroup } from "../../value-objects";
export type InvoiceTaxTotal = {};
export class InvoiceTaxes extends Collection<InvoiceTaxGroup> {
constructor(items: InvoiceTaxGroup[] = [], totalItems: number | null = null) {
super(items, totalItems);
}
public getIVAAmount(): InvoiceAmount {
return this.getAll().reduce(
(total, tax) => total.add(taxableAmount.percentage(tax.percentage)),
InvoiceAmount.zero(taxableAmount.currencyCode)
);
}
public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: InvoiceAmount): InvoiceAmount {
const currencyCode = taxableAmount.currencyCode;
return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => {
return taxableAmount.percentage(itemTax.percentage).add(totalAmount);
}, InvoiceAmount.zero(currencyCode));
}
public getTaxesAmountByTaxes(taxableAmount: InvoiceAmount): InvoiceTaxTotal[] {
return this.getAll().map((taxItem) => ({
taxableAmount,
tax: taxItem,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount),
}));
}
public getCodesToString(): string {
return this.getAll()
.map((taxItem) => taxItem.code)
.join(", ");
}
}

View File

@ -1,14 +1,9 @@
export * from "./invoice-address-type.vo"; export * from "./invoice-address-type.vo";
export * from "./invoice-amount.vo"; export * from "./invoice-amount.vo";
export * from "./invoice-discount-percentage.vo";
export * from "./invoice-number.vo"; export * from "./invoice-number.vo";
export * from "./invoice-recipient"; export * from "./invoice-recipient";
export * from "./invoice-serie.vo"; export * from "./invoice-serie.vo";
export * from "./invoice-status.vo"; export * from "./invoice-status.vo";
export * from "./invoice-tax-group.vo";
export * from "./invoice-tax-percentage.vo";
export * from "./item-amount.vo"; export * from "./item-amount.vo";
export * from "./item-description.vo"; export * from "./item-description.vo";
export * from "./item-discount-percentage.vo";
export * from "./item-quantity.vo"; export * from "./item-quantity.vo";
export * from "./item-tax-percentage.vo";

View File

@ -1,19 +0,0 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type InvoiceDiscountPercentageProps = Pick<PercentageProps, "value">;
export class InvoiceDiscountPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: InvoiceDiscountPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: InvoiceDiscountPercentage.DEFAULT_SCALE,
});
}
static zero() {
return InvoiceDiscountPercentage.create({ value: 0 }).data;
}
}

View File

@ -1,143 +0,0 @@
import type { Tax } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { ProformaItemTaxGroup } from "../../proformas/value-objects/proforma-item-tax-group.vo";
import { InvoiceAmount } from "./invoice-amount.vo";
export type InvoiceTaxGroupProps = {
taxableAmount: InvoiceAmount;
iva: Tax;
rec: Maybe<Tax>; // si existe
retention: Maybe<Tax>; // si existe
};
export class InvoiceTaxGroup extends ValueObject<InvoiceTaxGroupProps> {
static create(props: InvoiceTaxGroupProps) {
return Result.ok(new InvoiceTaxGroup(props));
}
/**
* Crea un grupo vacío a partir de un ItemTaxGroup (línea)
*/
static fromItem(lineTaxes: ProformaItemTaxGroup, taxableAmount: InvoiceAmount): InvoiceTaxGroup {
const iva = lineTaxes.iva.unwrap(); // iva siempre obligatorio
const rec = lineTaxes.rec;
const retention = lineTaxes.retention;
return new InvoiceTaxGroup({
iva,
rec,
retention,
taxableAmount,
});
}
calculateAmounts() {
const taxableAmount = this.props.taxableAmount;
const ivaAmount = taxableAmount.percentage(this.props.iva.percentage);
const recAmount = this.props.rec.match(
(rec) => taxableAmount.percentage(rec.percentage),
() => InvoiceAmount.zero(taxableAmount.currencyCode)
);
const retentionAmount = this.props.retention.match(
(retention) => taxableAmount.percentage(retention.percentage).multiply(-1),
() => InvoiceAmount.zero(taxableAmount.currencyCode)
);
const totalAmount = ivaAmount.add(recAmount).add(retentionAmount);
return { ivaAmount, recAmount, retentionAmount, totalAmount };
}
get iva(): Tax {
return this.props.iva;
}
get rec(): Maybe<Tax> {
return this.props.rec;
}
get retention(): Maybe<Tax> {
return this.props.retention;
}
get taxableAmount(): InvoiceAmount {
return this.props.taxableAmount;
}
/**
* Clave única del grupo: iva|rec|ret
*/
public getKey(): string {
const iva = this.props.iva.code;
const rec = this.props.rec.match(
(t) => t.code,
() => ""
);
const retention = this.props.retention.match(
(t) => t.code,
() => ""
);
return `${iva}|${rec}|${retention}`;
}
/**
* Suma una base imponible a este grupo.
*
* Devuelve un nuevo InvoiceTaxGroup (inmutabilidad).
*/
public addTaxable(amount: InvoiceAmount): InvoiceTaxGroup {
return new InvoiceTaxGroup({
...this.props,
taxableAmount: this.props.taxableAmount.add(amount),
});
}
/**
* Devuelve únicamente los códigos existentes: ["iva_21", "rec_5_2"]
*/
public getCodesArray(): string[] {
const codes: string[] = [];
// IVA
codes.push(this.props.iva.code);
this.props.rec.match(
(t) => codes.push(t.code),
() => {
//
}
);
this.props.retention.match(
(t) => codes.push(t.code),
() => {
//
}
);
return codes;
}
/**
* Devuelve una cadena tipo: "iva_21, rec_5_2"
*/
public getCodesToString(): string {
return this.getCodesArray().join(", ");
}
getProps() {
return this.props;
}
toPrimitive() {
return this.getProps();
}
}

View File

@ -1,19 +0,0 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type InvoiceTaxPercentageProps = Pick<PercentageProps, "value">;
export class InvoiceTaxPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: InvoiceTaxPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: InvoiceTaxPercentage.DEFAULT_SCALE,
});
}
static zero() {
return InvoiceTaxPercentage.create({ value: 0 }).data;
}
}

View File

@ -1,19 +0,0 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type ItemTaxPercentageProps = Pick<PercentageProps, "value">;
export class ItemTaxPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: ItemTaxPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: ItemTaxPercentage.DEFAULT_SCALE,
});
}
static zero() {
return ItemTaxPercentage.create({ value: 0 }).data;
}
}

View File

@ -1,3 +1,4 @@
import type { DiscountPercentage } from "@erp/core/api";
import { import {
AggregateRoot, AggregateRoot,
type CurrencyCode, type CurrencyCode,
@ -50,7 +51,7 @@ export type IssuedInvoiceProps = {
subtotalAmount: InvoiceAmount; subtotalAmount: InvoiceAmount;
itemsDiscountAmount: InvoiceAmount; itemsDiscountAmount: InvoiceAmount;
globalDiscountPercentage: Percentage; globalDiscountPercentage: DiscountPercentage;
globalDiscountAmount: InvoiceAmount; globalDiscountAmount: InvoiceAmount;
totalDiscountAmount: InvoiceAmount; totalDiscountAmount: InvoiceAmount;

View File

@ -1,18 +1,8 @@
import { import type { DiscountPercentage, TaxPercentage } from "@erp/core/api";
type CurrencyCode, import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd";
DomainEntity,
type LanguageCode,
type Percentage,
type UniqueID,
} from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import type { import type { ItemAmount, ItemDescription, ItemQuantity } from "../../../common";
ItemAmount,
ItemDescription,
ItemDiscountPercentage,
ItemQuantity,
} from "../../../common";
/** /**
* *
@ -32,10 +22,10 @@ export type IssuedInvoiceItemProps = {
subtotalAmount: ItemAmount; subtotalAmount: ItemAmount;
itemDiscountPercentage: Maybe<ItemDiscountPercentage>; itemDiscountPercentage: Maybe<DiscountPercentage>;
itemDiscountAmount: ItemAmount; itemDiscountAmount: ItemAmount;
globalDiscountPercentage: Maybe<ItemDiscountPercentage>; globalDiscountPercentage: Maybe<DiscountPercentage>;
globalDiscountAmount: ItemAmount; globalDiscountAmount: ItemAmount;
totalDiscountAmount: ItemAmount; totalDiscountAmount: ItemAmount;
@ -43,15 +33,15 @@ export type IssuedInvoiceItemProps = {
taxableAmount: ItemAmount; taxableAmount: ItemAmount;
ivaCode: Maybe<string>; ivaCode: Maybe<string>;
ivaPercentage: Maybe<ItemDiscountPercentage>; ivaPercentage: Maybe<DiscountPercentage>;
ivaAmount: ItemAmount; ivaAmount: ItemAmount;
recCode: Maybe<string>; recCode: Maybe<string>;
recPercentage: Maybe<ItemDiscountPercentage>; recPercentage: Maybe<DiscountPercentage>;
recAmount: ItemAmount; recAmount: ItemAmount;
retentionCode: Maybe<string>; retentionCode: Maybe<string>;
retentionPercentage: Maybe<ItemDiscountPercentage>; retentionPercentage: Maybe<DiscountPercentage>;
retentionAmount: ItemAmount; retentionAmount: ItemAmount;
taxesAmount: ItemAmount; taxesAmount: ItemAmount;
@ -136,7 +126,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
public get ivaCode(): Maybe<string> { public get ivaCode(): Maybe<string> {
return this.props.ivaCode; return this.props.ivaCode;
} }
public get ivaPercentage(): Maybe<Percentage> { public get ivaPercentage(): Maybe<TaxPercentage> {
return this.props.ivaPercentage; return this.props.ivaPercentage;
} }
public get ivaAmount(): ItemAmount { public get ivaAmount(): ItemAmount {
@ -146,7 +136,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
public get recCode(): Maybe<string> { public get recCode(): Maybe<string> {
return this.props.recCode; return this.props.recCode;
} }
public get recPercentage(): Maybe<Percentage> { public get recPercentage(): Maybe<TaxPercentage> {
return this.props.recPercentage; return this.props.recPercentage;
} }
public get recAmount(): ItemAmount { public get recAmount(): ItemAmount {
@ -156,7 +146,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
public get retentionCode(): Maybe<string> { public get retentionCode(): Maybe<string> {
return this.props.retentionCode; return this.props.retentionCode;
} }
public get retentionPercentage(): Maybe<Percentage> { public get retentionPercentage(): Maybe<TaxPercentage> {
return this.props.retentionPercentage; return this.props.retentionPercentage;
} }
public get retentionAmount(): ItemAmount { public get retentionAmount(): ItemAmount {

View File

@ -1,3 +1,4 @@
import type { TaxPercentage } from "@erp/core/api";
import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd"; import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
@ -12,11 +13,11 @@ export type IssuedInvoiceTaxProps = {
recCode: Maybe<string>; recCode: Maybe<string>;
recPercentage: Maybe<Percentage>; recPercentage: Maybe<Percentage>;
recAmount: Maybe<InvoiceAmount>; recAmount: InvoiceAmount;
retentionCode: Maybe<string>; retentionCode: Maybe<string>;
retentionPercentage: Maybe<Percentage>; retentionPercentage: Maybe<Percentage>;
retentionAmount: Maybe<InvoiceAmount>; retentionAmount: InvoiceAmount;
taxesAmount: InvoiceAmount; taxesAmount: InvoiceAmount;
}; };
@ -36,7 +37,7 @@ export class IssuedInvoiceTax extends DomainEntity<IssuedInvoiceTaxProps> {
public get ivaCode(): string { public get ivaCode(): string {
return this.props.ivaCode; return this.props.ivaCode;
} }
public get ivaPercentage(): Percentage { public get ivaPercentage(): TaxPercentage {
return this.props.ivaPercentage; return this.props.ivaPercentage;
} }
public get ivaAmount(): InvoiceAmount { public get ivaAmount(): InvoiceAmount {
@ -46,20 +47,20 @@ export class IssuedInvoiceTax extends DomainEntity<IssuedInvoiceTaxProps> {
public get recCode(): Maybe<string> { public get recCode(): Maybe<string> {
return this.props.recCode; return this.props.recCode;
} }
public get recPercentage(): Maybe<Percentage> { public get recPercentage(): Maybe<TaxPercentage> {
return this.props.recPercentage; return this.props.recPercentage;
} }
public get recAmount(): Maybe<InvoiceAmount> { public get recAmount(): InvoiceAmount {
return this.props.recAmount; return this.props.recAmount;
} }
public get retentionCode(): Maybe<string> { public get retentionCode(): Maybe<string> {
return this.props.retentionCode; return this.props.retentionCode;
} }
public get retentionPercentage(): Maybe<Percentage> { public get retentionPercentage(): Maybe<TaxPercentage> {
return this.props.retentionPercentage; return this.props.retentionPercentage;
} }
public get retentionAmount(): Maybe<InvoiceAmount> { public get retentionAmount(): InvoiceAmount {
return this.props.retentionAmount; return this.props.retentionAmount;
} }

View File

@ -1,3 +1,4 @@
import type { DiscountPercentage } from "@erp/core/api";
import { import {
AggregateRoot, AggregateRoot,
type CurrencyCode, type CurrencyCode,
@ -8,7 +9,7 @@ import {
type UniqueID, type UniqueID,
type UtcDate, type UtcDate,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Collection, type Maybe, Result } from "@repo/rdx-utils"; import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import type { InvoicePaymentMethod } from "../../common/entities"; import type { InvoicePaymentMethod } from "../../common/entities";
import { import {
@ -17,11 +18,10 @@ import {
type InvoiceRecipient, type InvoiceRecipient,
type InvoiceSerie, type InvoiceSerie,
type InvoiceStatus, type InvoiceStatus,
InvoiceTaxGroup,
type ItemAmount, type ItemAmount,
} from "../../common/value-objects"; } from "../../common/value-objects";
import type { ProformaTaxes } from "../entities";
import { ProformaItems } from "../entities/proforma-items"; import { ProformaItems } from "../entities/proforma-items";
import { type IProformaTaxTotals, ProformaTaxCalculator } from "../services";
export type ProformaProps = { export type ProformaProps = {
companyId: UniqueID; companyId: UniqueID;
@ -46,27 +46,51 @@ export type ProformaProps = {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: ProformaItems; items: ProformaItems;
globalDiscountPercentage: Percentage; globalDiscountPercentage: DiscountPercentage;
}; };
export interface IProforma extends AggregateRoot<ProformaProps> { export interface IProformaTotals {
getTaxes: ProformaTaxes; subtotalAmount: InvoiceAmount;
getSubtotalAmount: InvoiceAmount; itemDiscountAmount: InvoiceAmount;
globalDiscountAmount: InvoiceAmount;
totalDiscountAmount: InvoiceAmount;
getItemsDiscountAmount: InvoiceAmount; taxableAmount: InvoiceAmount;
getGlobalDiscountPercentage: Percentage;
getGlobalDiscountAmount: InvoiceAmount;
getTotalDiscountAmount: InvoiceAmount;
getTaxableAmount: InvoiceAmount; ivaAmount: InvoiceAmount;
recAmount: InvoiceAmount;
retentionAmount: InvoiceAmount;
getIvaAmount: InvoiceAmount; taxesAmount: InvoiceAmount;
getRecAmount: InvoiceAmount; totalAmount: InvoiceAmount;
getRetentionAmount: InvoiceAmount; }
getTaxesAmount: InvoiceAmount; export interface IProforma {
getTotalAmount: InvoiceAmount; companyId: UniqueID;
status: InvoiceStatus;
series: Maybe<InvoiceSerie>;
invoiceNumber: InvoiceNumber;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
customerId: UniqueID;
recipient: Maybe<InvoiceRecipient>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
paymentMethod: Maybe<InvoicePaymentMethod>;
items: ProformaItems;
taxes(): Collection<IProformaTaxTotals>;
totals(): IProformaTotals;
} }
export type ProformaPatchProps = Partial<Omit<ProformaProps, "companyId" | "items">> & { export type ProformaPatchProps = Partial<Omit<ProformaProps, "companyId" | "items">> & {
@ -142,10 +166,6 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
return this.props.status; return this.props.status;
} }
canTransitionTo(nextStatus: string): boolean {
return this.props.status.canTransitionTo(nextStatus);
}
public get series(): Maybe<InvoiceSerie> { public get series(): Maybe<InvoiceSerie> {
return this.props.series; return this.props.series;
} }
@ -207,6 +227,41 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
return this.paymentMethod.isSome(); return this.paymentMethod.isSome();
} }
// Cálculos
/**
* @summary Calcula todos los totales de factura a partir de los totales de las líneas.
* La cabecera NO recalcula lógica de porcentaje toda la lógica está en Item/Items.
*/
public totals(): IProformaTotals {
const itemsTotals = this.items.totals();
return {
subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount),
itemDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount),
globalDiscountAmount: this.toInvoiceAmount(itemsTotals.globalDiscountAmount),
totalDiscountAmount: this.toInvoiceAmount(itemsTotals.totalDiscountAmount),
taxableAmount: this.toInvoiceAmount(itemsTotals.taxableAmount),
ivaAmount: this.toInvoiceAmount(itemsTotals.ivaAmount),
recAmount: this.toInvoiceAmount(itemsTotals.recAmount),
retentionAmount: this.toInvoiceAmount(itemsTotals.retentionAmount),
taxesAmount: this.toInvoiceAmount(itemsTotals.taxesAmount),
totalAmount: this.toInvoiceAmount(itemsTotals.totalAmount),
} as const;
}
public taxes(): Collection<IProformaTaxTotals> {
return new ProformaTaxCalculator(this.items).calculate();
}
public getProps(): ProformaProps {
return this.props;
}
// Helpers // Helpers
/** /**
@ -218,101 +273,4 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
currency_code: this.currencyCode.code, currency_code: this.currencyCode.code,
}).data; }).data;
} }
// Cálculos
/**
* @summary Calcula todos los totales de factura a partir de los totales de las líneas.
* La cabecera NO recalcula lógica de porcentaje toda la lógica está en Item/Items.
*/
public calculateAllAmounts() {
const itemsTotals = this.items.calculateAllAmounts();
const subtotalAmount = this.toInvoiceAmount(itemsTotals.subtotalAmount);
const itemDiscountAmount = this.toInvoiceAmount(itemsTotals.itemDiscountAmount);
const globalDiscountAmount = this.toInvoiceAmount(itemsTotals.globalDiscountAmount);
const totalDiscountAmount = this.toInvoiceAmount(itemsTotals.totalDiscountAmount);
const taxableAmount = this.toInvoiceAmount(itemsTotals.taxableAmount);
const taxesAmount = this.toInvoiceAmount(itemsTotals.taxesAmount);
const totalAmount = this.toInvoiceAmount(itemsTotals.totalAmount);
const taxGroups = this.getTaxes();
return {
subtotalAmount,
itemDiscountAmount,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
taxesAmount,
totalAmount,
taxGroups,
} as const;
}
// Métodos públicos
public getProps(): ProformaProps {
return this.props;
}
public getSubtotalAmount(): InvoiceAmount {
return this.calculateAllAmounts().subtotalAmount;
}
public getItemDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().itemDiscountAmount;
}
public getGlobalDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().globalDiscountAmount;
}
public getTotalDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().totalDiscountAmount;
}
public getTaxableAmount(): InvoiceAmount {
return this.calculateAllAmounts().taxableAmount;
}
public getTaxesAmount(): InvoiceAmount {
return this.calculateAllAmounts().taxesAmount;
}
public getTotalAmount(): InvoiceAmount {
return this.calculateAllAmounts().totalAmount;
}
/**
* @summary Agrupa impuestos a nivel factura usando el trío (iva|rec|ret),
* construyendo InvoiceTaxGroup desde los datos de los ítems.
*/
public getTaxes(): Collection<InvoiceTaxGroup> {
const map = this.items.groupTaxesByCode();
const groups: InvoiceTaxGroup[] = [];
for (const [, entry] of map.entries()) {
const { taxes, taxable } = entry;
const iva = taxes.iva.unwrap(); // IVA siempre obligatorio
const rec = taxes.rec; // Maybe<Tax>
const retention = taxes.retention; // Maybe<Tax>
const taxableAmount = this.toInvoiceAmount(taxable);
const group = InvoiceTaxGroup.create({
iva,
rec,
retention,
taxableAmount,
}).data;
groups.push(group);
}
return new Collection(groups);
}
} }

View File

@ -1,2 +1 @@
export * from "./proforma-items"; export * from "./proforma-items";
export * from "./proforma-taxes";

View File

@ -1,14 +1,9 @@
import type { Tax } from "@erp/core/api"; import { DiscountPercentage, type Tax, type TaxPercentage } from "@erp/core/api";
import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd"; import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import { import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common";
ItemAmount, import type { ProformaItemTaxes } from "../../value-objects/proforma-item-taxes.vo";
type ItemDescription,
ItemDiscountPercentage,
ItemQuantity,
} from "../../../common";
import type { ProformaItemTaxGroup } from "../../value-objects/proforma-item-tax-group.vo";
/** /**
* *
@ -33,60 +28,63 @@ export type ProformaItemProps = {
quantity: Maybe<ItemQuantity>; // Cantidad de unidades quantity: Maybe<ItemQuantity>; // Cantidad de unidades
unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura
itemDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de línea itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea
taxes: ProformaItemTaxGroup; taxes: ProformaItemTaxes;
// Estos campos vienen de la cabecera, // Estos campos vienen de la cabecera,
// pero se necesitan para cálculos y representaciones de la línea. // pero se necesitan para cálculos y representaciones de la línea.
globalDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de la cabecera globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
}; };
export interface IProformaItem extends ProformaItemProps { export interface IProformaItemTotals {
subtotalAmount: ItemAmount;
itemDiscountAmount: ItemAmount;
globalDiscountAmount: ItemAmount;
totalDiscountAmount: ItemAmount;
taxableAmount: ItemAmount;
ivaAmount: ItemAmount;
recAmount: ItemAmount;
retentionAmount: ItemAmount;
taxesAmount: ItemAmount;
totalAmount: ItemAmount;
}
export interface IProformaItem {
description: Maybe<ItemDescription>; description: Maybe<ItemDescription>;
isValued: boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado" languageCode: LanguageCode;
currencyCode: CurrencyCode;
quantity: Maybe<ItemQuantity>; quantity: Maybe<ItemQuantity>;
unitAmount: Maybe<ItemAmount>; unitAmount: Maybe<ItemAmount>;
getSubtotalAmount: Maybe<ItemAmount>; taxes: ProformaItemTaxes;
itemDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de línea itemDiscountPercentage: Maybe<DiscountPercentage>; // Descuento en línea
getItemDiscountAmount: Maybe<ItemAmount>; globalDiscountPercentage: DiscountPercentage; // Descuento en cabecera
globalDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de la cabecera ivaCode(): Maybe<string>;
getGlobalDiscountAmount: Maybe<ItemAmount>; ivaPercentage(): Maybe<TaxPercentage>;
getTotalDiscountAmount: Maybe<ItemAmount>; recCode(): Maybe<string>;
recPercentage(): Maybe<TaxPercentage>;
getTaxableAmount: Maybe<ItemAmount>; retentionCode(): Maybe<string>;
retentionPercentage(): Maybe<TaxPercentage>;
getIva: Maybe<Tax>; totals(): IProformaItemTotals;
getIvaCode: Maybe<string>;
getIvaPercentage: Maybe<ItemDiscountPercentage>;
getIvaAmount: ItemAmount;
getRec: Maybe<Tax>; isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
getRecCode: Maybe<string>;
getRecPercentage: Maybe<ItemDiscountPercentage>;
getRecAmount: ItemAmount;
getRetention: Maybe<Tax>;
getRetentionCode: Maybe<string>;
getRetentionPercentage: Maybe<ItemDiscountPercentage>;
getRetentionAmount: ItemAmount;
getTaxesAmount: ItemAmount;
getTotalAmount: ItemAmount;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
} }
export class ProformaItem extends DomainEntity<ProformaItemProps> { export class ProformaItem extends DomainEntity<ProformaItemProps> implements IProformaItem {
public static create(props: ProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> { public static create(props: ProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> {
const item = new ProformaItem(props, id); const item = new ProformaItem(props, id);
@ -101,16 +99,18 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
super(props, id); super(props, id);
} }
// Getters
get isValued(): boolean {
return this.props.quantity.isSome() || this.props.unitAmount.isSome();
}
get description() { get description() {
return this.props.description; return this.props.description;
} }
get languageCode() {
return this.props.languageCode;
}
get currencyCode() {
return this.props.currencyCode;
}
get quantity() { get quantity() {
return this.props.quantity; return this.props.quantity;
} }
@ -131,14 +131,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
return this.props.taxes; return this.props.taxes;
} }
get languageCode() {
return this.props.languageCode;
}
get currencyCode() {
return this.props.currencyCode;
}
getProps(): ProformaItemProps { getProps(): ProformaItemProps {
return this.props; return this.props;
} }
@ -147,93 +139,87 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
return this.getProps(); return this.getProps();
} }
// Getters específicos para cálculos y representaciones // Cálculos y representaciones
// Todos a 4 decimales
public getSubtotalAmount(): ItemAmount { public isValued(): boolean {
return this.calculateAllAmounts().subtotalAmount; return this.props.quantity.isSome() || this.props.unitAmount.isSome();
} }
public getItemDiscountAmount(): ItemAmount { public subtotalAmount(): ItemAmount {
return this.calculateAllAmounts().itemDiscountAmount; if (!this.isValued()) {
return ItemAmount.zero(this.currencyCode.code);
}
const quantity = this.quantity.unwrap();
const unitAmount = this.unitAmount.unwrap();
return unitAmount.multiply(quantity);
} }
public getGlobalDiscountAmount(): ItemAmount { public iva(): Maybe<Tax> {
return this.calculateAllAmounts().globalDiscountAmount;
}
public getTotalDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().totalDiscountAmount;
}
public getTaxableAmount(): ItemAmount {
return this.calculateAllAmounts().taxableAmount;
}
public getIva(): Maybe<Tax> {
return this.taxes.iva; return this.taxes.iva;
} }
public getIvaCode(): Maybe<string> { public ivaCode(): Maybe<string> {
return this.taxes.iva.map((tax) => tax.code); return this.taxes.iva.map((tax) => tax.code);
} }
public getIvaPercentage(): Maybe<ItemDiscountPercentage> { public ivaPercentage(): Maybe<DiscountPercentage> {
return this.taxes.iva.map((tax) => tax.percentage); return this.taxes.iva.map((tax) => tax.percentage);
} }
public getIvaAmount(): ItemAmount { public rec(): Maybe<Tax> {
return this.calculateAllAmounts().ivaAmount;
}
public getRec(): Maybe<Tax> {
return this.taxes.rec; return this.taxes.rec;
} }
public getRecCode(): Maybe<string> { public recCode(): Maybe<string> {
return this.taxes.rec.map((tax) => tax.code); return this.taxes.rec.map((tax) => tax.code);
} }
public getRecPercentage(): Maybe<ItemDiscountPercentage> { public recPercentage(): Maybe<DiscountPercentage> {
return this.taxes.rec.map((tax) => tax.percentage); return this.taxes.rec.map((tax) => tax.percentage);
} }
public getIndividualTaxAmounts() { public retention(): Maybe<Tax> {
const { ivaAmount, recAmount, retentionAmount } = this.calculateAllAmounts(); return this.taxes.retention;
return { ivaAmount, recAmount, retentionAmount };
} }
public getTaxesAmount(): ItemAmount { public retentionCode(): Maybe<string> {
return this.calculateAllAmounts().taxesAmount; return this.taxes.retention.map((tax) => tax.code);
} }
public getTotalAmount(): ItemAmount { public retentionPercentage(): Maybe<DiscountPercentage> {
return this.calculateAllAmounts().totalAmount; return this.taxes.retention.map((tax) => tax.percentage);
} }
// Ayudantes // Cálculos / Ayudantes
/** /**
* @summary Helper puro para calcular el subtotal. * @summary Helper puro para calcular el subtotal.
*/ */
private _calculateSubtotalAmount(): ItemAmount { private _calculateSubtotalAmount(): ItemAmount {
const qty = this.quantity.match( if (!this.isValued()) {
(quantity) => quantity, return ItemAmount.zero(this.currencyCode.code);
() => ItemQuantity.zero() }
);
const unit = this.unitAmount.match( const quantity = this.quantity.unwrap();
(unitAmount) => unitAmount, const unitAmount = this.unitAmount.unwrap();
() => ItemAmount.zero(this.currencyCode.code)
); return unitAmount.multiply(quantity);
return unit.multiply(qty);
} }
/** /**
* @summary Helper puro para calcular el descuento de línea. * @summary Helper puro para calcular el descuento de línea.
*/ */
private _calculateItemDiscountAmount(subtotal: ItemAmount): ItemAmount { private _calculateItemDiscountAmount(subtotal: ItemAmount): ItemAmount {
if (!this.isValued() || this.props.itemDiscountPercentage.isNone()) {
return ItemAmount.zero(this.currencyCode.code);
}
const discountPercentage = this.props.itemDiscountPercentage.match( const discountPercentage = this.props.itemDiscountPercentage.match(
(discount) => discount, (discount) => discount,
() => ItemDiscountPercentage.zero() () => DiscountPercentage.zero()
); );
return subtotal.percentage(discountPercentage); return subtotal.percentage(discountPercentage);
@ -246,13 +232,13 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
subtotalAmount: ItemAmount, subtotalAmount: ItemAmount,
discountAmount: ItemAmount discountAmount: ItemAmount
): ItemAmount { ): ItemAmount {
if (!this.isValued()) {
return ItemAmount.zero(this.currencyCode.code);
}
const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount); const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount);
const globalDiscount = this.props.globalDiscountPercentage.match( const globalDiscount = this.props.globalDiscountPercentage;
(discount) => discount,
() => ItemDiscountPercentage.zero()
);
return amountAfterLineDiscount.percentage(globalDiscount); return amountAfterLineDiscount.percentage(globalDiscount);
} }
@ -267,8 +253,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
return itemDiscountAmount.add(globalDiscountAmount); return itemDiscountAmount.add(globalDiscountAmount);
} }
// Cálculos
/** /**
* @summary Cálculo centralizado de todos los valores intermedios. * @summary Cálculo centralizado de todos los valores intermedios.
* @returns Devuelve un objeto inmutable con todos los valores necesarios: * @returns Devuelve un objeto inmutable con todos los valores necesarios:
@ -284,7 +268,7 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
* - totalAmount * - totalAmount
* *
*/ */
public calculateAllAmounts() { public totals(): IProformaItemTotals {
const subtotalAmount = this._calculateSubtotalAmount(); const subtotalAmount = this._calculateSubtotalAmount();
const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount); const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount);
@ -320,6 +304,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
taxesAmount, taxesAmount,
totalAmount, totalAmount,
} as const; };
} }
} }

View File

@ -1,63 +1,52 @@
import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import type { DiscountPercentage } from "@erp/core/api";
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ItemAmount, ItemDiscountPercentage, type ItemTaxGroup } from "../../../common"; import { ItemAmount } from "../../../common";
import type { ProformaItem } from "./proforma-item.entity"; import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity";
export type ProformaItemsProps = { export type ProformaItemsProps = {
items?: ProformaItem[]; items?: ProformaItem[];
languageCode: LanguageCode;
currencyCode: CurrencyCode; // Estos campos vienen de la cabecera,
globalDiscountPercentage: Percentage; // pero se necesitan para cálculos y representaciones de la línea.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
}; };
export class ProformaItems extends Collection<ProformaItem> { export interface IProformaItems extends Collection<IProformaItem> {
private languageCode!: LanguageCode; valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
private currencyCode!: CurrencyCode; totals(): IProformaItemTotals;
private globalDiscountPercentage!: Percentage;
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
}
export class ProformaItems extends Collection<ProformaItem> implements IProformaItems {
public readonly languageCode!: LanguageCode;
public readonly currencyCode!: CurrencyCode;
public readonly globalDiscountPercentage!: DiscountPercentage;
constructor(props: ProformaItemsProps) { constructor(props: ProformaItemsProps) {
super(props.items ?? []); super(props.items ?? []);
this.languageCode = props.languageCode; this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
this.globalDiscountPercentage = props.globalDiscountPercentage; this.globalDiscountPercentage = props.globalDiscountPercentage;
this.ensureSameCurrencyAndLanguage(this.items);
} }
public static create(props: ProformaItemsProps): ProformaItems { public static create(props: ProformaItemsProps): ProformaItems {
return new ProformaItems(props); return new ProformaItems(props);
} }
// Helpers public valued(): IProformaItem[] {
return this.filter((item) => item.isValued());
private _sumAmounts(selector: (item: ProformaItem) => ItemAmount): ItemAmount {
return this.getAll().reduce(
(acc, item) => acc.add(selector(item)),
ItemAmount.zero(this.currencyCode.code)
);
} }
/**
* @summary Helper puro para sumar impuestos individuales por tipo.
*/
private _calculateIndividualTaxes() {
let iva = ItemAmount.zero(this.currencyCode.code);
let rec = ItemAmount.zero(this.currencyCode.code);
let retention = ItemAmount.zero(this.currencyCode.code);
for (const item of this.getAll()) {
const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts();
iva = iva.add(ivaAmount);
rec = rec.add(recAmount);
retention = retention.add(retentionAmount);
}
return { iva, rec, retention };
}
//
/** /**
* @summary Añade un nuevo ítem a la colección. * @summary Añade un nuevo ítem a la colección.
* @param item - El ítem de factura a añadir. * @param item - El ítem de factura a añadir.
@ -67,18 +56,13 @@ export class ProformaItems extends Collection<ProformaItem> {
* los de la colección. Si no coinciden, el método devuelve `false` sin modificar * los de la colección. Si no coinciden, el método devuelve `false` sin modificar
* la colección. * la colección.
*/ */
add(item: ProformaItem): boolean { public add(item: ProformaItem): boolean {
// Antes de añadir un nuevo item, debo comprobar que el item a añadir // Antes de añadir un nuevo item, debo comprobar que el item a añadir
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
const same = const same =
this.languageCode.equals(item.languageCode) && this.languageCode.equals(item.languageCode) &&
this.currencyCode.equals(item.currencyCode) && this.currencyCode.equals(item.currencyCode) &&
this.globalDiscountPercentage.equals( this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
item.globalDiscountPercentage.match(
(v) => v,
() => ItemDiscountPercentage.zero()
)
);
if (!same) return false; if (!same) return false;
@ -92,7 +76,7 @@ export class ProformaItems extends Collection<ProformaItem> {
* @remarks * @remarks
* Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos. * Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos.
*/ */
public calculateAllAmounts() { public totals(): IProformaItemTotals {
let subtotalAmount = ItemAmount.zero(this.currencyCode.code); let subtotalAmount = ItemAmount.zero(this.currencyCode.code);
let itemDiscountAmount = ItemAmount.zero(this.currencyCode.code); let itemDiscountAmount = ItemAmount.zero(this.currencyCode.code);
@ -109,7 +93,7 @@ export class ProformaItems extends Collection<ProformaItem> {
let totalAmount = ItemAmount.zero(this.currencyCode.code); let totalAmount = ItemAmount.zero(this.currencyCode.code);
for (const item of this.getAll()) { for (const item of this.getAll()) {
const amounts = item.calculateAllAmounts(); const amounts = item.totals();
// Subtotales // Subtotales
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); subtotalAmount = subtotalAmount.add(amounts.subtotalAmount);
@ -152,114 +136,11 @@ export class ProformaItems extends Collection<ProformaItem> {
} as const; } as const;
} }
public getSubtotalAmount(): ItemAmount { private ensureSameCurrencyAndLanguage(items: IProformaItem[]): void {
return this.calculateAllAmounts().subtotalAmount; for (const item of items) {
} if (!item.currencyCode.equals(this.currencyCode)) {
throw new Error("[ProformaItems] All items must share the same currency.");
public getItemDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().itemDiscountAmount;
}
public getGlobalDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().globalDiscountAmount;
}
public getTotalDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().totalDiscountAmount;
}
public getTaxableAmount(): ItemAmount {
return this.calculateAllAmounts().taxableAmount;
}
public getTaxesAmount(): ItemAmount {
return this.calculateAllAmounts().taxesAmount;
}
public getTotalAmount(): ItemAmount {
return this.calculateAllAmounts().totalAmount;
}
/**
* @summary Recalcula totales agrupando por el trío iva, rec y retención.
*/
public groupTaxesByCode() {
const map = new Map<
string,
{
taxes: ItemTaxGroup;
taxable: ItemAmount;
ivaAmount: ItemAmount;
recAmount: ItemAmount;
retentionAmount: ItemAmount;
taxesAmount: ItemAmount;
} }
>();
for (const item of this.getAll()) {
const amounts = item.calculateAllAmounts();
const taxable = amounts.taxableAmount;
const { ivaAmount, recAmount, retentionAmount, taxesAmount } = amounts;
const taxes = item.taxes;
const ivaCode = taxes.iva.match(
(t) => t.code,
() => ""
);
const recCode = taxes.rec.match(
(t) => t.code,
() => ""
);
const retCode = taxes.retention.match(
(t) => t.code,
() => ""
);
// Clave del grupo: combinación IVA|REC|RET
const key = `${ivaCode}|${recCode}|${retCode}`;
const prev = map.get(key) ?? {
taxes,
taxable: ItemAmount.zero(taxable.currencyCode),
ivaAmount: ItemAmount.zero(taxable.currencyCode),
recAmount: ItemAmount.zero(taxable.currencyCode),
retentionAmount: ItemAmount.zero(taxable.currencyCode),
taxesAmount: ItemAmount.zero(taxable.currencyCode),
};
map.set(key, {
taxes,
taxable: prev.taxable.add(taxable),
ivaAmount: prev.ivaAmount.add(ivaAmount),
recAmount: prev.recAmount.add(recAmount),
retentionAmount: prev.retentionAmount.add(retentionAmount),
taxesAmount: prev.taxesAmount.add(taxesAmount),
});
} }
return map;
// Devuelve grupos dinámicos del VO existente (InvoiceTaxGroup)
// Nota: necesitas construir InvoiceTaxGroup aquí o en Proforma.getTaxes().
// Para mantener el ejemplo acotado, se devuelve el map y Proforma lo transforma,
// pero puedes construir aquí directamente si prefieres.
/*return new Collection(
[...map.values()].map((entry) => {
const iva = entry.taxes.iva.unwrap();
const rec = entry.taxes.rec;
const retention = entry.taxes.retention;
// Convertimos a InvoiceAmount en el agregado (o aquí si tienes acceso)
// Aquí asumimos que InvoiceTaxGroup acepta ItemAmount/InvoiceAmount según tu implementación.
// Ajusta según tu VO real.
return InvoiceTaxGroup.create({
iva,
rec,
retention,
taxableAmount: entry.taxable.toInvoiceAmount(), // si existe helper; si no, lo haces en Proforma
}).data;
})
);*/
} }
} }

View File

@ -1,2 +0,0 @@
export * from "./proforma-tax.entity";
export * from "./proforma-taxes.collection";

View File

@ -1,70 +0,0 @@
import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../../common";
export type ProformaTaxProps = {
taxableAmount: InvoiceAmount;
ivaCode: string;
ivaPercentage: Percentage;
ivaAmount: InvoiceAmount;
recCode: Maybe<string>;
recPercentage: Maybe<Percentage>;
recAmount: Maybe<InvoiceAmount>;
retentionCode: Maybe<string>;
retentionPercentage: Maybe<Percentage>;
retentionAmount: Maybe<InvoiceAmount>;
taxesAmount: InvoiceAmount;
};
export class ProformaTax extends DomainEntity<ProformaTaxProps> {
public static create(props: ProformaTaxProps, id?: UniqueID): Result<ProformaTax, Error> {
return Result.ok(new ProformaTax(props, id));
}
public get taxableAmount(): InvoiceAmount {
return this.props.taxableAmount;
}
public get ivaCode(): string {
return this.props.ivaCode;
}
public get ivaPercentage(): Percentage {
return this.props.ivaPercentage;
}
public get ivaAmount(): InvoiceAmount {
return this.props.ivaAmount;
}
public get recCode(): Maybe<string> {
return this.props.recCode;
}
public get recPercentage(): Maybe<Percentage> {
return this.props.recPercentage;
}
public get recAmount(): Maybe<InvoiceAmount> {
return this.props.recAmount;
}
public get retentionCode(): Maybe<string> {
return this.props.retentionCode;
}
public get retentionPercentage(): Maybe<Percentage> {
return this.props.retentionPercentage;
}
public get retentionAmount(): Maybe<InvoiceAmount> {
return this.props.retentionAmount;
}
public get taxesAmount(): InvoiceAmount {
return this.props.taxesAmount;
}
public getProps(): ProformaTaxProps {
return this.props;
}
}

View File

@ -1,25 +0,0 @@
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import type { ProformaTax } from "./proforma-tax.entity";
export type ProformaTaxesProps = {
taxes?: ProformaTax[];
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
export class ProformaTaxes extends Collection<ProformaTax> {
private _languageCode!: LanguageCode;
private _currencyCode!: CurrencyCode;
constructor(props: ProformaTaxesProps) {
super(props.taxes ?? []);
this._languageCode = props.languageCode;
this._currencyCode = props.currencyCode;
}
public static create(props: ProformaTaxesProps): ProformaTaxes {
return new ProformaTaxes(props);
}
}

View File

@ -0,0 +1 @@
export * from "./proforma-tax-calculator";

View File

@ -0,0 +1,55 @@
import type { TaxPercentage } from "@erp/core/api";
import type { Maybe } from "@repo/rdx-utils";
import type { IProformaTaxTotals } from "./proforma-tax-calculator";
/**
* Orden determinista:
* 1) IVA (code, %)
* 2) REC (None primero) (code, %)
* 3) RET (None primero) (code, %)
*/
export function proformaCompareTaxTotals(a: IProformaTaxTotals, b: IProformaTaxTotals): number {
const byIvaCode = compareCode(a.ivaCode, b.ivaCode);
if (byIvaCode !== 0) return byIvaCode;
const byIvaPct = comparePct(a.ivaPercentage, b.ivaPercentage);
if (byIvaPct !== 0) return byIvaPct;
const byRecCode = compareMaybeCodeNoneFirst(a.recCode, b.recCode);
if (byRecCode !== 0) return byRecCode;
const byRecPct = compareMaybePctNoneFirst(a.recPercentage, b.recPercentage);
if (byRecPct !== 0) return byRecPct;
const byRetCode = compareMaybeCodeNoneFirst(a.retentionCode, b.retentionCode);
if (byRetCode !== 0) return byRetCode;
return compareMaybePctNoneFirst(a.retentionPercentage, b.retentionPercentage);
}
function compareCode(a: string, b: string): number {
// Ajusta a tu VO: .value / .code / .toString()
return a.localeCompare(b);
}
function comparePct(a: TaxPercentage, b: TaxPercentage): number {
// Ajusta a tu VO: a.value puede ser number/bigint/string
return a.value - b.value;
}
function compareMaybeCodeNoneFirst(a: Maybe<string>, b: Maybe<string>): number {
if (a.isNone() && b.isNone()) return 0;
if (a.isNone() && b.isSome()) return -1; // None primero
if (a.isSome() && b.isNone()) return 1;
return compareCode(a.unwrap(), b.unwrap());
}
function compareMaybePctNoneFirst(a: Maybe<TaxPercentage>, b: Maybe<TaxPercentage>): number {
if (a.isNone() && b.isNone()) return 0;
if (a.isNone() && b.isSome()) return -1; // None primero
if (a.isSome() && b.isNone()) return 1;
return comparePct(a.unwrap(), b.unwrap());
}

View File

@ -0,0 +1,82 @@
import type { TaxPercentage } from "@erp/core/api";
import { Maybe } from "@repo/rdx-utils";
import { type InvoiceAmount, ItemAmount } from "../../common";
import type { IProformaItems } from "../entities";
type TaxGroupState = {
taxableAmount: ItemAmount;
ivaCode: string;
ivaPercentage: TaxPercentage;
ivaAmount: ItemAmount;
recCode: Maybe<string>;
recPercentage: Maybe<TaxPercentage>;
recAmount: InvoiceAmount;
retentionCode: Maybe<string>;
retentionPercentage: Maybe<TaxPercentage>;
retentionAmount: InvoiceAmount;
};
/**
* Agrupa líneas valoradas por trío (IVA/REC/RET) y acumula importes en scale 4.
*
* Reglas:
* - IVA siempre existe en líneas valoradas (incluye IVA EXENTO 0%).
* - REC y RETENTION pueden ser None.
* - No se recalculan porcentajes (se suma lo ya calculado por línea).
*/
export function proformaComputeTaxGroups(items: IProformaItems): Map<string, TaxGroupState> {
const map = new Map<string, TaxGroupState>();
const currency = items.currencyCode;
for (const item of items.valued()) {
const iva = item.taxes.iva.unwrap(); // siempre existe
const rec = item.taxes.rec;
const retention = item.taxes.retention;
const key = buildTaxGroupKey(iva, rec, retention);
if (!map.has(key)) {
map.set(key, {
taxableAmount: ItemAmount.zero(currency.code),
ivaCode: iva.code,
ivaPercentage: iva.percentage,
ivaAmount: ItemAmount.zero(currency.code),
recCode: rec.isSome() ? Maybe.some(rec.unwrap().code) : Maybe.none(),
recPercentage: rec.isSome() ? Maybe.some(rec.unwrap().percentage) : Maybe.none(),
recAmount: ItemAmount.zero(currency.code),
retentionCode: retention.isSome() ? Maybe.some(retention.unwrap().code) : Maybe.none(),
retentionPercentage: retention.isSome()
? Maybe.some(retention.unwrap().percentage)
: Maybe.none(),
retentionAmount: ItemAmount.zero(currency.code),
});
}
const g = map.get(key)!;
const itemTotals = item.totals();
g.taxableAmount = g.taxableAmount.add(itemTotals.taxableAmount);
g.ivaAmount = g.ivaAmount.add(itemTotals.ivaAmount);
g.recAmount = g.recAmount.add(itemTotals.recAmount);
g.retentionAmount = g.retentionAmount.add(itemTotals.retentionAmount);
}
return map;
}
function buildTaxGroupKey(iva: any, rec: any, retention: any): string {
const recPart = rec.isSome() ? `${rec.unwrap().code}-${rec.unwrap().percentage.value}` : "NULL";
const retentionPart = retention.isSome()
? `${retention.unwrap().code}-${retention.unwrap().percentage.value}`
: "NULL";
return `${iva.code}-${iva.percentage.value}|${recPart}|${retentionPart}`;
}

View File

@ -0,0 +1,72 @@
import type { TaxPercentage } from "@erp/core/api";
import { Collection, type Maybe } from "@repo/rdx-utils";
import { InvoiceAmount, type ItemAmount } from "../../common";
import type { IProformaItems } from "../entities";
import { proformaCompareTaxTotals } from "./proforma-compare-tax-totals";
import { proformaComputeTaxGroups } from "./proforma-compute-tax-groups";
export interface IProformaTaxTotals {
taxableAmount: InvoiceAmount;
ivaCode: string;
ivaPercentage: TaxPercentage;
ivaAmount: InvoiceAmount;
recCode: Maybe<string>;
recPercentage: Maybe<TaxPercentage>;
recAmount: InvoiceAmount;
retentionCode: Maybe<string>;
retentionPercentage: Maybe<TaxPercentage>;
retentionAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
}
export class ProformaTaxCalculator {
constructor(private readonly items: IProformaItems) {}
public calculate(): Collection<IProformaTaxTotals> {
const groups = proformaComputeTaxGroups(this.items);
const currencyCode = this.items.currencyCode;
const rows = Array.from(groups.values()).map((g) => {
const taxableAmount = this.toInvoiceAmount(g.taxableAmount);
const ivaAmount = this.toInvoiceAmount(g.ivaAmount);
const recAmount = this.toInvoiceAmount(g.recAmount);
const retentionAmount = this.toInvoiceAmount(g.retentionAmount);
const taxesAmount = ivaAmount.add(recAmount).subtract(retentionAmount);
return {
taxableAmount,
ivaCode: g.ivaCode,
ivaPercentage: g.ivaPercentage,
ivaAmount,
recCode: g.recCode,
recPercentage: g.recPercentage,
recAmount,
retentionCode: g.retentionCode,
retentionPercentage: g.retentionPercentage,
retentionAmount,
taxesAmount,
} as const;
});
rows.sort(proformaCompareTaxTotals);
return new Collection(rows);
}
private toInvoiceAmount(amount: ItemAmount): InvoiceAmount {
return InvoiceAmount.create({
value: amount.convertScale(InvoiceAmount.DEFAULT_SCALE).value,
currency_code: this.items.currencyCode.code,
}).data;
}
}

View File

@ -1,2 +1 @@
export * from "./proforma-item-tax-group.vo"; export * from "./proforma-item-taxes.vo";
export * from "./proforma-tax-group.vo";

View File

@ -4,15 +4,45 @@ import { type Maybe, Result } from "@repo/rdx-utils";
import { ItemAmount } from "../../common/value-objects"; import { ItemAmount } from "../../common/value-objects";
export interface ProformaItemTaxGroupProps { export type ProformaItemTaxesProps = {
iva: Maybe<Tax>; // si existe iva: Maybe<Tax>; // si existe
rec: Maybe<Tax>; // si existe rec: Maybe<Tax>; // si existe
retention: Maybe<Tax>; // si existe retention: Maybe<Tax>; // si existe
};
export interface IProformaItemTaxes {
iva: Maybe<Tax>; // si existe
rec: Maybe<Tax>; // si existe
retention: Maybe<Tax>; // si existe
toKey(): string; // Clave para representar un trío.
} }
export class ProformaItemTaxGroup extends ValueObject<ProformaItemTaxGroupProps> { export class ProformaItemTaxes
static create(props: ProformaItemTaxGroupProps) { extends ValueObject<ProformaItemTaxesProps>
return Result.ok(new ProformaItemTaxGroup(props)); implements IProformaItemTaxes
{
static create(props: ProformaItemTaxesProps) {
return Result.ok(new ProformaItemTaxes(props));
}
toKey(): string {
const ivaCode = this.props.iva.match(
(iva) => iva.code,
() => "#"
);
const recCode = this.props.rec.match(
(rec) => rec.code,
() => "#"
);
const retentionCode = this.props.retention.match(
(retention) => retention.code,
() => "#"
);
return `${ivaCode};${recCode};${retentionCode}`;
} }
calculateAmounts(taxableAmount: ItemAmount) { calculateAmounts(taxableAmount: ItemAmount) {
@ -46,37 +76,6 @@ export class ProformaItemTaxGroup extends ValueObject<ProformaItemTaxGroupProps>
return this.props.retention; return this.props.retention;
} }
public getCodesArray(): string[] {
const codes: string[] = [];
this.props.iva.match(
(iva) => codes.push(iva.code),
() => {
//
}
);
this.props.rec.match(
(rec) => codes.push(rec.code),
() => {
//
}
);
this.props.retention.match(
(retention) => codes.push(retention.code),
() => {
//
}
);
return codes;
}
public getCodesToString(): string {
return this.getCodesArray().join(", ");
}
getProps() { getProps() {
return this.props; return this.props;
} }

View File

@ -1,66 +0,0 @@
import type { Tax } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../common";
export type ProformaTaxGroupProps = {
taxableAmount: InvoiceAmount;
iva: Tax;
ivaAmount: InvoiceAmount;
rec: Maybe<Tax>; // si existe
recAmount: Maybe<InvoiceAmount>;
retention: Maybe<Tax>; // si existe
retentionAmount: Maybe<InvoiceAmount>;
taxesAmount: InvoiceAmount;
};
export class ProformaTaxGroup extends ValueObject<ProformaTaxGroupProps> {
static create(props: ProformaTaxGroupProps) {
return Result.ok(new ProformaTaxGroup(props));
}
get taxableAmount(): InvoiceAmount {
return this.props.taxableAmount;
}
get iva(): Tax {
return this.props.iva;
}
get ivaAmount(): InvoiceAmount {
return this.props.ivaAmount;
}
get rec(): Maybe<Tax> {
return this.props.rec;
}
get recAmount(): Maybe<InvoiceAmount> {
return this.props.recAmount;
}
get retention(): Maybe<Tax> {
return this.props.retention;
}
get retentionAmount(): Maybe<InvoiceAmount> {
return this.props.retentionAmount;
}
get taxesAmount(): InvoiceAmount {
return this.props.taxesAmount;
}
getProps() {
return this.props;
}
toPrimitive() {
return this.getProps();
}
}

View File

@ -37,12 +37,12 @@ export class CustomerInvoiceItemModel extends Model<
declare subtotal_amount_scale: number; declare subtotal_amount_scale: number;
// Discount percentage // Discount percentage
declare discount_percentage_value: CreationOptional<number | null>; declare item_discount_percentage_value: CreationOptional<number | null>;
declare discount_percentage_scale: number; declare item_discount_percentage_scale: number;
// Discount amount // Discount amount
declare discount_amount_value: number; declare item_discount_amount_value: number;
declare discount_amount_scale: number; declare item_discount_amount_scale: number;
// Porcentaje de descuento global proporcional a esta línea. // Porcentaje de descuento global proporcional a esta línea.
declare global_discount_percentage_value: CreationOptional<number | null>; declare global_discount_percentage_value: CreationOptional<number | null>;
@ -86,7 +86,6 @@ export class CustomerInvoiceItemModel extends Model<
declare retention_percentage_value: CreationOptional<number | null>; declare retention_percentage_value: CreationOptional<number | null>;
declare retention_percentage_scale: number; declare retention_percentage_scale: number;
// Retention amount
declare retention_amount_value: number; declare retention_amount_value: number;
declare retention_amount_scale: number; declare retention_amount_scale: number;
@ -185,25 +184,25 @@ export default (database: Sequelize) => {
defaultValue: 4, defaultValue: 4,
}, },
discount_percentage_value: { item_discount_percentage_value: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
discount_percentage_scale: { item_discount_percentage_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 2,
}, },
discount_amount_value: { item_discount_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}, },
discount_amount_scale: { item_discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 4, defaultValue: 4,

View File

@ -35,7 +35,6 @@ export class CustomerInvoiceTaxModel extends Model<
declare iva_percentage_scale: number; declare iva_percentage_scale: number;
// IVA amount // IVA amount
declare iva_amount_value: number; declare iva_amount_value: number;
declare iva_amount_scale: number; declare iva_amount_scale: number;

View File

@ -73,7 +73,7 @@ export class CustomerInvoiceModel extends Model<
declare items_discount_amount_scale: number; declare items_discount_amount_scale: number;
// Global/header discount percentage // Global/header discount percentage
declare global_discount_percentage_value: number; declare global_discount_percentage_value: CreationOptional<number | null>;
declare global_discount_percentage_scale: number; declare global_discount_percentage_scale: number;
// Global/header discount amount // Global/header discount amount

View File

@ -2,7 +2,6 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
LanguageCode, LanguageCode,
Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
UtcDate, UtcDate,
@ -16,6 +15,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IIssuedInvoiceDomainMapper } from "../../../../../../application"; import type { IIssuedInvoiceDomainMapper } from "../../../../../../application";
import { import {
DiscountPercentage,
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
@ -68,8 +68,6 @@ export class SequelizeIssuedInvoiceDomainMapper
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
const isIssuedInvoice = Boolean(raw.is_proforma);
const proformaId = extractOrPushError( const proformaId = extractOrPushError(
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)), maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
"proforma_id", "proforma_id",
@ -181,9 +179,8 @@ export class SequelizeIssuedInvoiceDomainMapper
// % descuento global (VO) // % descuento global (VO)
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ DiscountPercentage.create({
value: Number(raw.global_discount_percentage_value ?? 0), value: Number(raw.global_discount_percentage_value ?? 0),
scale: Number(raw.global_discount_percentage_scale ?? 2),
}), }),
"global_discount_percentage_value", "global_discount_percentage_value",
errors errors
@ -265,7 +262,6 @@ export class SequelizeIssuedInvoiceDomainMapper
invoiceId, invoiceId,
companyId, companyId,
customerId, customerId,
isIssuedInvoice,
proformaId, proformaId,
status, status,
series, series,
@ -294,50 +290,42 @@ export class SequelizeIssuedInvoiceDomainMapper
} }
public mapToDomain( public mapToDomain(
source: CustomerInvoiceModel, raw: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<IssuedInvoice, Error> { ): Result<IssuedInvoice, Error> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
const attributes = this._mapAttributesToDomain(source, { errors, ...params }); const attributes = this._mapAttributesToDomain(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include) // 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(source, { const recipientResult = this._recipientMapper.mapToDomain(raw, {
errors, errors,
attributes, attributes,
...params, ...params,
}); });
// 3) Verifactu (snapshot en la factura o include) // 3) Verifactu (snapshot en la factura o include)
const verifactuResult = this._verifactuMapper.mapToDomain(source.verifactu, { const verifactuResult = this._verifactuMapper.mapToDomain(raw.verifactu, {
errors, errors,
attributes, attributes,
...params, ...params,
}); });
// 4) Items (colección) // 4) Items (colección)
const itemsResults = this._itemsMapper.mapToDomainCollection( const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
source.items, errors,
source.items.length, attributes,
{ ...params,
errors, });
attributes,
...params,
}
);
// 5) Taxes (colección) // 5) Taxes (colección)
const taxesResults = this._taxesMapper.mapToDomainCollection( const taxesResults = this._taxesMapper.mapToDomainCollection(raw.taxes, raw.taxes.length, {
source.taxes, errors,
source.taxes.length, attributes,
{ ...params,
errors, });
attributes,
...params,
}
);
// 6) Si hubo errores de mapeo, devolvemos colección de validación // 6) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
@ -499,7 +487,6 @@ export class SequelizeIssuedInvoiceDomainMapper
reference: maybeToNullable(source.reference, (reference) => reference), reference: maybeToNullable(source.reference, (reference) => reference),
description: maybeToNullable(source.description, (description) => description), description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable( payment_method_id: maybeToNullable(

View File

@ -13,6 +13,7 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
DiscountPercentage,
type IssuedInvoice, type IssuedInvoice,
IssuedInvoiceItem, IssuedInvoiceItem,
type IssuedInvoiceItemProps, type IssuedInvoiceItemProps,
@ -94,25 +95,25 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
); );
const itemDiscountPercentage = extractOrPushError( const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.discount_percentage_value, (v) => maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v }) ItemDiscountPercentage.create({ value: v })
), ),
`items[${index}].discount_percentage_value`, `items[${index}].item_discount_percentage_value`,
errors errors
); );
const itemDiscountAmount = extractOrPushError( const itemDiscountAmount = extractOrPushError(
ItemAmount.create({ ItemAmount.create({
value: raw.discount_amount_value, value: raw.item_discount_amount_value,
currency_code: attributes.currencyCode?.code, currency_code: attributes.currencyCode?.code,
}), }),
`items[${index}].discount_amount_value`, `items[${index}].item_discount_amount_value`,
errors errors
); );
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.global_discount_percentage_value, (v) => maybeFromNullableResult(raw.global_discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v }) DiscountPercentage.create({ value: v })
), ),
`items[${index}].global_discount_percentage_value`, `items[${index}].global_discount_percentage_value`,
errors errors
@ -357,16 +358,16 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
subtotal_amount_value: source.subtotalAmount.toPrimitive().value, subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale, subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
discount_percentage_value: maybeToNullable( item_discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage, source.itemDiscountPercentage,
(v) => v.toPrimitive().value (v) => v.toPrimitive().value
), ),
discount_percentage_scale: item_discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, ItemDiscountPercentage.DEFAULT_SCALE,
discount_amount_value: source.itemDiscountAmount.toPrimitive().value, item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
global_discount_percentage_value: maybeToNullable( global_discount_percentage_value: maybeToNullable(
source.globalDiscountPercentage, source.globalDiscountPercentage,

View File

@ -1,6 +1,7 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { import {
Percentage,
UniqueID, UniqueID,
ValidationErrorCollection, ValidationErrorCollection,
type ValidationErrorDetail, type ValidationErrorDetail,
@ -14,10 +15,12 @@ import { Result } from "@repo/rdx-utils";
import { import {
InvoiceAmount, InvoiceAmount,
InvoiceTaxPercentage,
type IssuedInvoice, type IssuedInvoice,
type IssuedInvoiceProps, type IssuedInvoiceProps,
IssuedInvoiceTax, IssuedInvoiceTax,
ItemAmount,
ItemDiscountPercentage,
TaxPercentage,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { import type {
CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxCreationAttributes,
@ -57,7 +60,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
} }
public mapToDomain( public mapToDomain(
source: CustomerInvoiceTaxModel, raw: CustomerInvoiceTaxModel,
params?: MapperParamsType params?: MapperParamsType
): Result<IssuedInvoiceTax, Error> { ): Result<IssuedInvoiceTax, Error> {
const { errors, index, attributes } = params as { const { errors, index, attributes } = params as {
@ -68,18 +71,18 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
const taxableAmount = extractOrPushError( const taxableAmount = extractOrPushError(
InvoiceAmount.create({ InvoiceAmount.create({
value: source.taxable_amount_value, value: raw.taxable_amount_value,
currency_code: attributes.currencyCode?.code, currency_code: attributes.currencyCode?.code,
}), }),
`taxes[${index}].taxable_amount_value`, `taxes[${index}].taxable_amount_value`,
errors errors
); );
const ivaCode = source.iva_code; const ivaCode = raw.iva_code;
const ivaPercentage = extractOrPushError( const ivaPercentage = extractOrPushError(
InvoiceTaxPercentage.create({ TaxPercentage.create({
value: source.iva_percentage_value, value: raw.iva_percentage_value,
}), }),
`taxes[${index}].iva_percentage_value`, `taxes[${index}].iva_percentage_value`,
errors errors
@ -87,52 +90,52 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
const ivaAmount = extractOrPushError( const ivaAmount = extractOrPushError(
InvoiceAmount.create({ InvoiceAmount.create({
value: source.iva_amount_value, value: raw.iva_amount_value,
currency_code: attributes.currencyCode?.code, currency_code: attributes.currencyCode?.code,
}), }),
`taxes[${index}].iva_amount_value`, `taxes[${index}].iva_amount_value`,
errors errors
); );
const recCode = maybeFromNullableOrEmptyString(source.rec_code); const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
const recPercentage = extractOrPushError( const recPercentage = extractOrPushError(
maybeFromNullableResult(source.rec_percentage_value, (value) => maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
InvoiceTaxPercentage.create({ value })
),
`taxes[${index}].rec_percentage_value`, `taxes[${index}].rec_percentage_value`,
errors errors
); );
const recAmount = extractOrPushError( const recAmount = extractOrPushError(
maybeFromNullableResult(source.rec_amount_value, (value) => InvoiceAmount.create({
InvoiceAmount.create({ value, currency_code: attributes.currencyCode?.code }) value: raw.rec_amount_value,
), currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].rec_amount_value`, `taxes[${index}].rec_amount_value`,
errors errors
); );
const retentionCode = maybeFromNullableOrEmptyString(source.retention_code); const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code);
const retentionPercentage = extractOrPushError( const retentionPercentage = extractOrPushError(
maybeFromNullableResult(source.retention_percentage_value, (value) => maybeFromNullableResult(raw.retention_percentage_value, (value) =>
InvoiceTaxPercentage.create({ value }) TaxPercentage.create({ value })
), ),
`taxes[${index}].retention_percentage_value`, `taxes[${index}].retention_percentage_value`,
errors errors
); );
const retentionAmount = extractOrPushError( const retentionAmount = extractOrPushError(
maybeFromNullableResult(source.retention_amount_value, (value) => InvoiceAmount.create({
InvoiceAmount.create({ value, currency_code: attributes.currencyCode?.code }) value: raw.retention_amount_value,
), currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].retention_amount_value`, `taxes[${index}].retention_amount_value`,
errors errors
); );
const taxesAmount = extractOrPushError( const taxesAmount = extractOrPushError(
InvoiceAmount.create({ InvoiceAmount.create({
value: source.taxes_amount_value, value: raw.taxes_amount_value,
currency_code: attributes.currencyCode?.code, currency_code: attributes.currencyCode?.code,
}), }),
`taxes[${index}].taxes_amount_value`, `taxes[${index}].taxes_amount_value`,
@ -208,10 +211,11 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value), rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
rec_percentage_scale: rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? 2, maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE,
rec_amount_value: maybeToNullable(source.recAmount, (v) => v.toPrimitive().value), rec_amount_value: source.recAmount.toPrimitive().value,
rec_amount_scale: maybeToNullable(source.recAmount, (v) => v.toPrimitive().scale) ?? 4, rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
// RET // RET
retention_code: maybeToNullableString(source.retentionCode), retention_code: maybeToNullableString(source.retentionCode),
@ -221,14 +225,12 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
(v) => v.toPrimitive().value (v) => v.toPrimitive().value
), ),
retention_percentage_scale: retention_percentage_scale:
maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? 2, maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ??
Percentage.DEFAULT_SCALE,
retention_amount_value: maybeToNullable( retention_amount_value: source.retentionAmount.toPrimitive().value,
source.retentionAmount,
(v) => v.toPrimitive().value
),
retention_amount_scale: retention_amount_scale:
maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4, source.retentionAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
// TOTAL // TOTAL
taxes_amount_value: source.taxesAmount.value, taxes_amount_value: source.taxesAmount.value,

View File

@ -2,7 +2,6 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
LanguageCode, LanguageCode,
Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
UtcDate, UtcDate,
@ -16,6 +15,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IProformaDomainMapper } from "../../../../../../application"; import type { IProformaDomainMapper } from "../../../../../../application";
import { import {
DiscountPercentage,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
InvoiceSerie, InvoiceSerie,
@ -46,86 +46,80 @@ export class SequelizeProformaDomainMapper
this._itemsMapper = new SequelizeProformaItemDomainMapper(params); this._itemsMapper = new SequelizeProformaItemDomainMapper(params);
this._recipientMapper = new SequelizeProformaRecipientDomainMapper(); this._recipientMapper = new SequelizeProformaRecipientDomainMapper();
this._taxesMapper = new SequelizeProformaTaxesDomainMapper(); this._taxesMapper = new SequelizeProformaTaxesDomainMapper(params);
} }
private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { private _mapAttributesToDomain(raw: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as { const { errors } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };
const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const customerId = extractOrPushError( const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
UniqueID.create(source.customer_id),
"customer_id",
errors
);
const isProforma = Boolean(source.is_proforma);
const proformaId = extractOrPushError( const proformaId = extractOrPushError(
maybeFromNullableResult(source.proforma_id, (v) => UniqueID.create(v)), maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
"proforma_id", "proforma_id",
errors errors
); );
const status = extractOrPushError(InvoiceStatus.create(source.status), "status", errors); const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError( const series = extractOrPushError(
maybeFromNullableResult(source.series, (v) => InvoiceSerie.create(v)), maybeFromNullableResult(raw.series, (v) => InvoiceSerie.create(v)),
"series", "series",
errors errors
); );
const invoiceNumber = extractOrPushError( const invoiceNumber = extractOrPushError(
InvoiceNumber.create(source.invoice_number), InvoiceNumber.create(raw.invoice_number),
"invoice_number", "invoice_number",
errors errors
); );
// Fechas // Fechas
const invoiceDate = extractOrPushError( const invoiceDate = extractOrPushError(
UtcDate.createFromISO(source.invoice_date), UtcDate.createFromISO(raw.invoice_date),
"invoice_date", "invoice_date",
errors errors
); );
const operationDate = extractOrPushError( const operationDate = extractOrPushError(
maybeFromNullableResult(source.operation_date, (v) => UtcDate.createFromISO(v)), maybeFromNullableResult(raw.operation_date, (v) => UtcDate.createFromISO(v)),
"operation_date", "operation_date",
errors errors
); );
// Idioma / divisa // Idioma / divisa
const languageCode = extractOrPushError( const languageCode = extractOrPushError(
LanguageCode.create(source.language_code), LanguageCode.create(raw.language_code),
"language_code", "language_code",
errors errors
); );
const currencyCode = extractOrPushError( const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code), CurrencyCode.create(raw.currency_code),
"currency_code", "currency_code",
errors errors
); );
// Textos opcionales // Textos opcionales
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableResult(source.reference, (value) => Result.ok(String(value))), maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))),
"reference", "reference",
errors errors
); );
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(source.description, (value) => Result.ok(String(value))), maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))),
"description", "description",
errors errors
); );
const notes = extractOrPushError( const notes = extractOrPushError(
maybeFromNullableResult(source.notes, (value) => TextValue.create(value)), maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)),
"notes", "notes",
errors errors
); );
@ -133,16 +127,16 @@ export class SequelizeProformaDomainMapper
// Método de pago (VO opcional con id + descripción) // Método de pago (VO opcional con id + descripción)
let paymentMethod = Maybe.none<InvoicePaymentMethod>(); let paymentMethod = Maybe.none<InvoicePaymentMethod>();
if (!isNullishOrEmpty(source.payment_method_id)) { if (!isNullishOrEmpty(raw.payment_method_id)) {
const paymentId = extractOrPushError( const paymentId = extractOrPushError(
UniqueID.create(String(source.payment_method_id)), UniqueID.create(String(raw.payment_method_id)),
"paymentMethod.id", "paymentMethod.id",
errors errors
); );
const paymentVO = extractOrPushError( const paymentVO = extractOrPushError(
InvoicePaymentMethod.create( InvoicePaymentMethod.create(
{ paymentDescription: String(source.payment_method_description ?? "") }, { paymentDescription: String(raw.payment_method_description ?? "") },
paymentId ?? undefined paymentId ?? undefined
), ),
"payment_method_description", "payment_method_description",
@ -154,13 +148,12 @@ export class SequelizeProformaDomainMapper
} }
} }
// % descuento (VO) // % descuento global (VO)
const discountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ DiscountPercentage.create({
value: Number(source.discount_percentage_value ?? 0), value: Number(raw.global_discount_percentage_value ?? 0),
scale: Number(source.discount_percentage_scale ?? 2),
}), }),
"discount_percentage_value", "global_discount_percentage_value",
errors errors
); );
@ -168,7 +161,6 @@ export class SequelizeProformaDomainMapper
invoiceId, invoiceId,
companyId, companyId,
customerId, customerId,
isProforma,
proformaId, proformaId,
status, status,
series, series,
@ -180,38 +172,35 @@ export class SequelizeProformaDomainMapper
notes, notes,
languageCode, languageCode,
currencyCode, currencyCode,
discountPercentage,
paymentMethod, paymentMethod,
globalDiscountPercentage,
}; };
} }
public mapToDomain( public mapToDomain(
source: CustomerInvoiceModel, raw: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<Proforma, Error> { ): Result<Proforma, Error> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
const attributes = this._mapAttributesToDomain(source, { errors, ...params }); const attributes = this._mapAttributesToDomain(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include) // 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(source, { const recipientResult = this._recipientMapper.mapToDomain(raw, {
errors, errors,
attributes, attributes,
...params, ...params,
}); });
// 3) Items (colección) // 3) Items (colección)
const itemsResults = this._itemsMapper.mapToDomainCollection( const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
source.items, errors,
source.items.length, attributes,
{ ...params,
errors, });
attributes,
...params,
}
);
// 4) Si hubo errores de mapeo, devolvemos colección de validación // 4) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
@ -225,14 +214,13 @@ export class SequelizeProformaDomainMapper
const items = ProformaItems.create({ const items = ProformaItems.create({
languageCode: attributes.languageCode!, languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.discountPercentage!, globalDiscountPercentage: attributes.globalDiscountPercentage!,
items: itemsResults.data.getAll(), items: itemsResults.data.getAll(),
}); });
const invoiceProps: ProformaProps = { const invoiceProps: ProformaProps = {
companyId: attributes.companyId!, companyId: attributes.companyId!,
isProforma: attributes.isProforma,
status: attributes.status!, status: attributes.status!,
series: attributes.series!, series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!, invoiceNumber: attributes.invoiceNumber!,
@ -249,7 +237,7 @@ export class SequelizeProformaDomainMapper
languageCode: attributes.languageCode!, languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.discountPercentage!, globalDiscountPercentage: attributes.globalDiscountPercentage!,
paymentMethod: attributes.paymentMethod!, paymentMethod: attributes.paymentMethod!,
@ -331,11 +319,12 @@ export class SequelizeProformaDomainMapper
company_id: source.companyId.toPrimitive(), company_id: source.companyId.toPrimitive(),
// Flags / estado / serie / número // Flags / estado / serie / número
is_proforma: source.isProforma, is_proforma: true,
status: source.status.toPrimitive(), status: source.status.toPrimitive(),
proforma_id: null,
series: maybeToNullable(source.series, (v) => v.toPrimitive()), series: maybeToNullable(source.series, (v) => v.toPrimitive()),
invoice_number: source.invoiceNumber.toPrimitive(), invoice_number: source.invoiceNumber.toPrimitive(),
invoice_date: source.invoiceDate.toPrimitive(), invoice_date: source.invoiceDate.toPrimitive(),
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()), operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
language_code: source.languageCode.toPrimitive(), language_code: source.languageCode.toPrimitive(),
@ -345,6 +334,15 @@ export class SequelizeProformaDomainMapper
description: maybeToNullable(source.description, (description) => description), description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().id
),
payment_method_description: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().payment_description
),
subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale, subtotal_amount_scale: allAmounts.subtotalAmount.scale,
@ -369,15 +367,6 @@ export class SequelizeProformaDomainMapper
total_amount_value: allAmounts.totalAmount.value, total_amount_value: allAmounts.totalAmount.value,
total_amount_scale: allAmounts.totalAmount.scale, total_amount_scale: allAmounts.totalAmount.scale,
payment_method_id: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().id
),
payment_method_description: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().payment_description
),
customer_id: source.customerId.toPrimitive(), customer_id: source.customerId.toPrimitive(),
...recipient, ...recipient,

View File

@ -1,3 +1,4 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd"; import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
@ -25,6 +26,21 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxCreationAttributes,
InvoiceTaxGroup InvoiceTaxGroup
> { > {
private taxCatalog!: JsonTaxCatalogProvider;
constructor(params: MapperParamsType) {
super();
const { taxCatalog } = params as {
taxCatalog: JsonTaxCatalogProvider;
};
if (!taxCatalog) {
throw new Error('taxCatalog not defined ("SequelizeProformaTaxesDomainMapper")');
}
this.taxCatalog = taxCatalog;
}
public mapToDomain( public mapToDomain(
source: CustomerInvoiceTaxModel, source: CustomerInvoiceTaxModel,
params?: MapperParamsType params?: MapperParamsType

View File

@ -28,6 +28,10 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"], "include": [
"src",
"../core/src/api/domain/value-objects/tax-percentage.vo.ts",
"../core/src/api/domain/value-objects/discount-percentage.vo.ts"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -1,6 +1,5 @@
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object"; import { ValueObject } from "./value-object";
interface TaxCodeProps { interface TaxCodeProps {
@ -28,7 +27,8 @@ export class TaxCode extends ValueObject<TaxCodeProps> {
} }
static create(value: string) { static create(value: string) {
const valueIsValid = TaxCode.validate(value); throw new Error("DEPRECATED -> ¿DEBERÍA USARSE STRING COMO EN LAS FACTURAS?");
/*const valueIsValid = TaxCode.validate(value);
if (!valueIsValid.success) { if (!valueIsValid.success) {
return Result.fail( return Result.fail(
@ -36,7 +36,7 @@ export class TaxCode extends ValueObject<TaxCodeProps> {
); );
} }
// biome-ignore lint/style/noNonNullAssertion: <explanation> // biome-ignore lint/style/noNonNullAssertion: <explanation>
return Result.ok(new TaxCode({ value: valueIsValid.data! })); return Result.ok(new TaxCode({ value: valueIsValid.data! }));*/
} }
getProps(): string { getProps(): string {

View File

@ -31,7 +31,7 @@ export class Collection<T> {
addCollection(collection: Collection<T>): boolean { addCollection(collection: Collection<T>): boolean {
this.items.push(...collection.items); this.items.push(...collection.items);
if (this.totalItems !== null) { if (this.totalItems !== null) {
this.totalItems = this.totalItems + collection.totalItems; this.totalItems += collection.totalItems;
} }
return true; return true;
} }

7
uecko-erp.code-workspace Normal file
View File

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}