.
This commit is contained in:
parent
e9824ecf80
commit
cc38faed94
@ -1,4 +1,4 @@
|
||||
import { Tax } from "../tax";
|
||||
import { Tax } from "../tax.vo";
|
||||
|
||||
describe("Tax Value Object", () => {
|
||||
describe("Creación", () => {
|
||||
@ -244,7 +244,7 @@ describe("Tax Value Object", () => {
|
||||
if (result.isSuccess) {
|
||||
const tax = result.value;
|
||||
const json = tax.toJSON();
|
||||
|
||||
|
||||
expect(json).toEqual({
|
||||
value: 2100,
|
||||
scale: 2,
|
||||
@ -285,7 +285,7 @@ describe("Tax Value Object", () => {
|
||||
if (result.isSuccess) {
|
||||
const tax = result.value;
|
||||
const props = tax.getProps();
|
||||
|
||||
|
||||
// Intentar modificar las propiedades debe fallar
|
||||
expect(() => {
|
||||
(props as any).value = 3000;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from "./tax";
|
||||
export * from "./discount-percentage.vo";
|
||||
export * from "./tax.vo";
|
||||
export * from "./tax-percentage.vo";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,21 @@
|
||||
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 { z } from "zod/v4";
|
||||
|
||||
const DEFAULT_SCALE = 2;
|
||||
const DEFAULT_MIN_VALUE = 0;
|
||||
const DEFAULT_MAX_VALUE = 100;
|
||||
import { TaxPercentage } from "./tax-percentage.vo";
|
||||
|
||||
const DEFAULT_MIN_SCALE = 0;
|
||||
const DEFAULT_MAX_SCALE = 4;
|
||||
const DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
|
||||
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 {
|
||||
code: string; // iva_21
|
||||
name: string; // 21% IVA
|
||||
value: number; // 2100
|
||||
scale: number; // 2
|
||||
}
|
||||
|
||||
export class Tax extends ValueObject<TaxProps> {
|
||||
@ -26,7 +27,7 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
|
||||
private static CODE_REGEX = /^[a-z0-9_:-]+$/;
|
||||
|
||||
private _percentage!: Percentage;
|
||||
private _percentage!: TaxPercentage;
|
||||
|
||||
protected static validate(values: TaxProps) {
|
||||
const schema = z.object({
|
||||
@ -55,19 +56,14 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
}
|
||||
|
||||
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) {
|
||||
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
|
||||
}
|
||||
|
||||
const realValue = value / 10 ** scale;
|
||||
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 }));
|
||||
return Result.ok(new Tax({ value, name, code }));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,7 +97,6 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
// Delegamos en create para reusar validación y límites
|
||||
return Tax.create({
|
||||
value: Number(item.value),
|
||||
scale: Number(item.scale) ?? Tax.DEFAULT_SCALE,
|
||||
name: item.name,
|
||||
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) {
|
||||
super(props);
|
||||
this._percentage = Percentage.create({
|
||||
this._percentage = TaxPercentage.create({
|
||||
value: this.props.value,
|
||||
scale: this.props.scale,
|
||||
}).data;
|
||||
}
|
||||
|
||||
@ -119,7 +113,7 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
return this.props.value;
|
||||
}
|
||||
get scale(): number {
|
||||
return this.props.scale;
|
||||
return Tax.DEFAULT_SCALE;
|
||||
}
|
||||
get name(): string {
|
||||
return this.props.name;
|
||||
@ -128,7 +122,7 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
return this.props.code;
|
||||
}
|
||||
|
||||
get percentage(): Percentage {
|
||||
get percentage(): TaxPercentage {
|
||||
return this._percentage;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export * from "./application-models";
|
||||
export * from "./di";
|
||||
export * from "./dtos";
|
||||
//export * from "./mappers";
|
||||
export * from "./mappers";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./invoice-payment-method";
|
||||
export * from "./invoice-taxes";
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./invoice-tax";
|
||||
export * from "./invoice-taxes";
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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(", ");
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,9 @@
|
||||
export * from "./invoice-address-type.vo";
|
||||
export * from "./invoice-amount.vo";
|
||||
export * from "./invoice-discount-percentage.vo";
|
||||
export * from "./invoice-number.vo";
|
||||
export * from "./invoice-recipient";
|
||||
export * from "./invoice-serie.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-description.vo";
|
||||
export * from "./item-discount-percentage.vo";
|
||||
export * from "./item-quantity.vo";
|
||||
export * from "./item-tax-percentage.vo";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import type { DiscountPercentage } from "@erp/core/api";
|
||||
import {
|
||||
AggregateRoot,
|
||||
type CurrencyCode,
|
||||
@ -50,7 +51,7 @@ export type IssuedInvoiceProps = {
|
||||
subtotalAmount: InvoiceAmount;
|
||||
|
||||
itemsDiscountAmount: InvoiceAmount;
|
||||
globalDiscountPercentage: Percentage;
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
globalDiscountAmount: InvoiceAmount;
|
||||
totalDiscountAmount: InvoiceAmount;
|
||||
|
||||
|
||||
@ -1,18 +1,8 @@
|
||||
import {
|
||||
type CurrencyCode,
|
||||
DomainEntity,
|
||||
type LanguageCode,
|
||||
type Percentage,
|
||||
type UniqueID,
|
||||
} from "@repo/rdx-ddd";
|
||||
import type { DiscountPercentage, TaxPercentage } from "@erp/core/api";
|
||||
import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type {
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
ItemQuantity,
|
||||
} from "../../../common";
|
||||
import type { ItemAmount, ItemDescription, ItemQuantity } from "../../../common";
|
||||
|
||||
/**
|
||||
*
|
||||
@ -32,10 +22,10 @@ export type IssuedInvoiceItemProps = {
|
||||
|
||||
subtotalAmount: ItemAmount;
|
||||
|
||||
itemDiscountPercentage: Maybe<ItemDiscountPercentage>;
|
||||
itemDiscountPercentage: Maybe<DiscountPercentage>;
|
||||
itemDiscountAmount: ItemAmount;
|
||||
|
||||
globalDiscountPercentage: Maybe<ItemDiscountPercentage>;
|
||||
globalDiscountPercentage: Maybe<DiscountPercentage>;
|
||||
globalDiscountAmount: ItemAmount;
|
||||
|
||||
totalDiscountAmount: ItemAmount;
|
||||
@ -43,15 +33,15 @@ export type IssuedInvoiceItemProps = {
|
||||
taxableAmount: ItemAmount;
|
||||
|
||||
ivaCode: Maybe<string>;
|
||||
ivaPercentage: Maybe<ItemDiscountPercentage>;
|
||||
ivaPercentage: Maybe<DiscountPercentage>;
|
||||
ivaAmount: ItemAmount;
|
||||
|
||||
recCode: Maybe<string>;
|
||||
recPercentage: Maybe<ItemDiscountPercentage>;
|
||||
recPercentage: Maybe<DiscountPercentage>;
|
||||
recAmount: ItemAmount;
|
||||
|
||||
retentionCode: Maybe<string>;
|
||||
retentionPercentage: Maybe<ItemDiscountPercentage>;
|
||||
retentionPercentage: Maybe<DiscountPercentage>;
|
||||
retentionAmount: ItemAmount;
|
||||
|
||||
taxesAmount: ItemAmount;
|
||||
@ -136,7 +126,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
|
||||
public get ivaCode(): Maybe<string> {
|
||||
return this.props.ivaCode;
|
||||
}
|
||||
public get ivaPercentage(): Maybe<Percentage> {
|
||||
public get ivaPercentage(): Maybe<TaxPercentage> {
|
||||
return this.props.ivaPercentage;
|
||||
}
|
||||
public get ivaAmount(): ItemAmount {
|
||||
@ -146,7 +136,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
|
||||
public get recCode(): Maybe<string> {
|
||||
return this.props.recCode;
|
||||
}
|
||||
public get recPercentage(): Maybe<Percentage> {
|
||||
public get recPercentage(): Maybe<TaxPercentage> {
|
||||
return this.props.recPercentage;
|
||||
}
|
||||
public get recAmount(): ItemAmount {
|
||||
@ -156,7 +146,7 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
|
||||
public get retentionCode(): Maybe<string> {
|
||||
return this.props.retentionCode;
|
||||
}
|
||||
public get retentionPercentage(): Maybe<Percentage> {
|
||||
public get retentionPercentage(): Maybe<TaxPercentage> {
|
||||
return this.props.retentionPercentage;
|
||||
}
|
||||
public get retentionAmount(): ItemAmount {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { TaxPercentage } from "@erp/core/api";
|
||||
import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
@ -12,11 +13,11 @@ export type IssuedInvoiceTaxProps = {
|
||||
|
||||
recCode: Maybe<string>;
|
||||
recPercentage: Maybe<Percentage>;
|
||||
recAmount: Maybe<InvoiceAmount>;
|
||||
recAmount: InvoiceAmount;
|
||||
|
||||
retentionCode: Maybe<string>;
|
||||
retentionPercentage: Maybe<Percentage>;
|
||||
retentionAmount: Maybe<InvoiceAmount>;
|
||||
retentionAmount: InvoiceAmount;
|
||||
|
||||
taxesAmount: InvoiceAmount;
|
||||
};
|
||||
@ -36,7 +37,7 @@ export class IssuedInvoiceTax extends DomainEntity<IssuedInvoiceTaxProps> {
|
||||
public get ivaCode(): string {
|
||||
return this.props.ivaCode;
|
||||
}
|
||||
public get ivaPercentage(): Percentage {
|
||||
public get ivaPercentage(): TaxPercentage {
|
||||
return this.props.ivaPercentage;
|
||||
}
|
||||
public get ivaAmount(): InvoiceAmount {
|
||||
@ -46,20 +47,20 @@ export class IssuedInvoiceTax extends DomainEntity<IssuedInvoiceTaxProps> {
|
||||
public get recCode(): Maybe<string> {
|
||||
return this.props.recCode;
|
||||
}
|
||||
public get recPercentage(): Maybe<Percentage> {
|
||||
public get recPercentage(): Maybe<TaxPercentage> {
|
||||
return this.props.recPercentage;
|
||||
}
|
||||
public get recAmount(): Maybe<InvoiceAmount> {
|
||||
public get recAmount(): InvoiceAmount {
|
||||
return this.props.recAmount;
|
||||
}
|
||||
|
||||
public get retentionCode(): Maybe<string> {
|
||||
return this.props.retentionCode;
|
||||
}
|
||||
public get retentionPercentage(): Maybe<Percentage> {
|
||||
public get retentionPercentage(): Maybe<TaxPercentage> {
|
||||
return this.props.retentionPercentage;
|
||||
}
|
||||
public get retentionAmount(): Maybe<InvoiceAmount> {
|
||||
public get retentionAmount(): InvoiceAmount {
|
||||
return this.props.retentionAmount;
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { DiscountPercentage } from "@erp/core/api";
|
||||
import {
|
||||
AggregateRoot,
|
||||
type CurrencyCode,
|
||||
@ -8,7 +9,7 @@ import {
|
||||
type UniqueID,
|
||||
type UtcDate,
|
||||
} 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 {
|
||||
@ -17,11 +18,10 @@ import {
|
||||
type InvoiceRecipient,
|
||||
type InvoiceSerie,
|
||||
type InvoiceStatus,
|
||||
InvoiceTaxGroup,
|
||||
type ItemAmount,
|
||||
} from "../../common/value-objects";
|
||||
import type { ProformaTaxes } from "../entities";
|
||||
import { ProformaItems } from "../entities/proforma-items";
|
||||
import { type IProformaTaxTotals, ProformaTaxCalculator } from "../services";
|
||||
|
||||
export type ProformaProps = {
|
||||
companyId: UniqueID;
|
||||
@ -46,27 +46,51 @@ export type ProformaProps = {
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: ProformaItems;
|
||||
globalDiscountPercentage: Percentage;
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
};
|
||||
|
||||
export interface IProforma extends AggregateRoot<ProformaProps> {
|
||||
getTaxes: ProformaTaxes;
|
||||
export interface IProformaTotals {
|
||||
subtotalAmount: InvoiceAmount;
|
||||
|
||||
getSubtotalAmount: InvoiceAmount;
|
||||
itemDiscountAmount: InvoiceAmount;
|
||||
globalDiscountAmount: InvoiceAmount;
|
||||
totalDiscountAmount: InvoiceAmount;
|
||||
|
||||
getItemsDiscountAmount: InvoiceAmount;
|
||||
getGlobalDiscountPercentage: Percentage;
|
||||
getGlobalDiscountAmount: InvoiceAmount;
|
||||
getTotalDiscountAmount: InvoiceAmount;
|
||||
taxableAmount: InvoiceAmount;
|
||||
|
||||
getTaxableAmount: InvoiceAmount;
|
||||
ivaAmount: InvoiceAmount;
|
||||
recAmount: InvoiceAmount;
|
||||
retentionAmount: InvoiceAmount;
|
||||
|
||||
getIvaAmount: InvoiceAmount;
|
||||
getRecAmount: InvoiceAmount;
|
||||
getRetentionAmount: InvoiceAmount;
|
||||
taxesAmount: InvoiceAmount;
|
||||
totalAmount: InvoiceAmount;
|
||||
}
|
||||
|
||||
getTaxesAmount: InvoiceAmount;
|
||||
getTotalAmount: InvoiceAmount;
|
||||
export interface IProforma {
|
||||
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">> & {
|
||||
@ -142,10 +166,6 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
return this.props.status;
|
||||
}
|
||||
|
||||
canTransitionTo(nextStatus: string): boolean {
|
||||
return this.props.status.canTransitionTo(nextStatus);
|
||||
}
|
||||
|
||||
public get series(): Maybe<InvoiceSerie> {
|
||||
return this.props.series;
|
||||
}
|
||||
@ -207,6 +227,41 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
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
|
||||
|
||||
/**
|
||||
@ -218,101 +273,4 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
currency_code: this.currencyCode.code,
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./proforma-items";
|
||||
export * from "./proforma-taxes";
|
||||
|
||||
@ -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 Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
ItemAmount,
|
||||
type ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
ItemQuantity,
|
||||
} from "../../../common";
|
||||
import type { ProformaItemTaxGroup } from "../../value-objects/proforma-item-tax-group.vo";
|
||||
import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common";
|
||||
import type { ProformaItemTaxes } from "../../value-objects/proforma-item-taxes.vo";
|
||||
|
||||
/**
|
||||
*
|
||||
@ -33,60 +28,63 @@ export type ProformaItemProps = {
|
||||
quantity: Maybe<ItemQuantity>; // Cantidad de unidades
|
||||
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,
|
||||
// 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
|
||||
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>;
|
||||
|
||||
isValued: boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
|
||||
languageCode: LanguageCode;
|
||||
currencyCode: CurrencyCode;
|
||||
|
||||
quantity: Maybe<ItemQuantity>;
|
||||
unitAmount: Maybe<ItemAmount>;
|
||||
|
||||
getSubtotalAmount: Maybe<ItemAmount>;
|
||||
taxes: ProformaItemTaxes;
|
||||
|
||||
itemDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de línea
|
||||
getItemDiscountAmount: Maybe<ItemAmount>;
|
||||
itemDiscountPercentage: Maybe<DiscountPercentage>; // Descuento en línea
|
||||
globalDiscountPercentage: DiscountPercentage; // Descuento en cabecera
|
||||
|
||||
globalDiscountPercentage: Maybe<ItemDiscountPercentage>; // % descuento de la cabecera
|
||||
getGlobalDiscountAmount: Maybe<ItemAmount>;
|
||||
ivaCode(): Maybe<string>;
|
||||
ivaPercentage(): Maybe<TaxPercentage>;
|
||||
|
||||
getTotalDiscountAmount: Maybe<ItemAmount>;
|
||||
recCode(): Maybe<string>;
|
||||
recPercentage(): Maybe<TaxPercentage>;
|
||||
|
||||
getTaxableAmount: Maybe<ItemAmount>;
|
||||
retentionCode(): Maybe<string>;
|
||||
retentionPercentage(): Maybe<TaxPercentage>;
|
||||
|
||||
getIva: Maybe<Tax>;
|
||||
getIvaCode: Maybe<string>;
|
||||
getIvaPercentage: Maybe<ItemDiscountPercentage>;
|
||||
getIvaAmount: ItemAmount;
|
||||
totals(): IProformaItemTotals;
|
||||
|
||||
getRec: Maybe<Tax>;
|
||||
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;
|
||||
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
|
||||
}
|
||||
|
||||
export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
export class ProformaItem extends DomainEntity<ProformaItemProps> implements IProformaItem {
|
||||
public static create(props: ProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> {
|
||||
const item = new ProformaItem(props, id);
|
||||
|
||||
@ -101,16 +99,18 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
super(props, id);
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
get isValued(): boolean {
|
||||
return this.props.quantity.isSome() || this.props.unitAmount.isSome();
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.props.description;
|
||||
}
|
||||
|
||||
get languageCode() {
|
||||
return this.props.languageCode;
|
||||
}
|
||||
|
||||
get currencyCode() {
|
||||
return this.props.currencyCode;
|
||||
}
|
||||
|
||||
get quantity() {
|
||||
return this.props.quantity;
|
||||
}
|
||||
@ -131,14 +131,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
return this.props.taxes;
|
||||
}
|
||||
|
||||
get languageCode() {
|
||||
return this.props.languageCode;
|
||||
}
|
||||
|
||||
get currencyCode() {
|
||||
return this.props.currencyCode;
|
||||
}
|
||||
|
||||
getProps(): ProformaItemProps {
|
||||
return this.props;
|
||||
}
|
||||
@ -147,93 +139,87 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
return this.getProps();
|
||||
}
|
||||
|
||||
// Getters específicos para cálculos y representaciones
|
||||
// Cálculos y representaciones
|
||||
// Todos a 4 decimales
|
||||
|
||||
public getSubtotalAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().subtotalAmount;
|
||||
public isValued(): boolean {
|
||||
return this.props.quantity.isSome() || this.props.unitAmount.isSome();
|
||||
}
|
||||
|
||||
public getItemDiscountAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().itemDiscountAmount;
|
||||
public subtotalAmount(): ItemAmount {
|
||||
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 {
|
||||
return this.calculateAllAmounts().globalDiscountAmount;
|
||||
}
|
||||
|
||||
public getTotalDiscountAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().totalDiscountAmount;
|
||||
}
|
||||
|
||||
public getTaxableAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().taxableAmount;
|
||||
}
|
||||
|
||||
public getIva(): Maybe<Tax> {
|
||||
public iva(): Maybe<Tax> {
|
||||
return this.taxes.iva;
|
||||
}
|
||||
|
||||
public getIvaCode(): Maybe<string> {
|
||||
public ivaCode(): Maybe<string> {
|
||||
return this.taxes.iva.map((tax) => tax.code);
|
||||
}
|
||||
|
||||
public getIvaPercentage(): Maybe<ItemDiscountPercentage> {
|
||||
public ivaPercentage(): Maybe<DiscountPercentage> {
|
||||
return this.taxes.iva.map((tax) => tax.percentage);
|
||||
}
|
||||
|
||||
public getIvaAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().ivaAmount;
|
||||
}
|
||||
|
||||
public getRec(): Maybe<Tax> {
|
||||
public rec(): Maybe<Tax> {
|
||||
return this.taxes.rec;
|
||||
}
|
||||
|
||||
public getRecCode(): Maybe<string> {
|
||||
public recCode(): Maybe<string> {
|
||||
return this.taxes.rec.map((tax) => tax.code);
|
||||
}
|
||||
|
||||
public getRecPercentage(): Maybe<ItemDiscountPercentage> {
|
||||
public recPercentage(): Maybe<DiscountPercentage> {
|
||||
return this.taxes.rec.map((tax) => tax.percentage);
|
||||
}
|
||||
|
||||
public getIndividualTaxAmounts() {
|
||||
const { ivaAmount, recAmount, retentionAmount } = this.calculateAllAmounts();
|
||||
return { ivaAmount, recAmount, retentionAmount };
|
||||
public retention(): Maybe<Tax> {
|
||||
return this.taxes.retention;
|
||||
}
|
||||
|
||||
public getTaxesAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().taxesAmount;
|
||||
public retentionCode(): Maybe<string> {
|
||||
return this.taxes.retention.map((tax) => tax.code);
|
||||
}
|
||||
|
||||
public getTotalAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().totalAmount;
|
||||
public retentionPercentage(): Maybe<DiscountPercentage> {
|
||||
return this.taxes.retention.map((tax) => tax.percentage);
|
||||
}
|
||||
|
||||
// Ayudantes
|
||||
// Cálculos / Ayudantes
|
||||
|
||||
/**
|
||||
* @summary Helper puro para calcular el subtotal.
|
||||
*/
|
||||
private _calculateSubtotalAmount(): ItemAmount {
|
||||
const qty = this.quantity.match(
|
||||
(quantity) => quantity,
|
||||
() => ItemQuantity.zero()
|
||||
);
|
||||
const unit = this.unitAmount.match(
|
||||
(unitAmount) => unitAmount,
|
||||
() => ItemAmount.zero(this.currencyCode.code)
|
||||
);
|
||||
return unit.multiply(qty);
|
||||
if (!this.isValued()) {
|
||||
return ItemAmount.zero(this.currencyCode.code);
|
||||
}
|
||||
|
||||
const quantity = this.quantity.unwrap();
|
||||
const unitAmount = this.unitAmount.unwrap();
|
||||
|
||||
return unitAmount.multiply(quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Helper puro para calcular el descuento de línea.
|
||||
*/
|
||||
private _calculateItemDiscountAmount(subtotal: ItemAmount): ItemAmount {
|
||||
if (!this.isValued() || this.props.itemDiscountPercentage.isNone()) {
|
||||
return ItemAmount.zero(this.currencyCode.code);
|
||||
}
|
||||
|
||||
const discountPercentage = this.props.itemDiscountPercentage.match(
|
||||
(discount) => discount,
|
||||
() => ItemDiscountPercentage.zero()
|
||||
() => DiscountPercentage.zero()
|
||||
);
|
||||
|
||||
return subtotal.percentage(discountPercentage);
|
||||
@ -246,13 +232,13 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
subtotalAmount: ItemAmount,
|
||||
discountAmount: ItemAmount
|
||||
): ItemAmount {
|
||||
if (!this.isValued()) {
|
||||
return ItemAmount.zero(this.currencyCode.code);
|
||||
}
|
||||
|
||||
const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount);
|
||||
|
||||
const globalDiscount = this.props.globalDiscountPercentage.match(
|
||||
(discount) => discount,
|
||||
() => ItemDiscountPercentage.zero()
|
||||
);
|
||||
|
||||
const globalDiscount = this.props.globalDiscountPercentage;
|
||||
return amountAfterLineDiscount.percentage(globalDiscount);
|
||||
}
|
||||
|
||||
@ -267,8 +253,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
return itemDiscountAmount.add(globalDiscountAmount);
|
||||
}
|
||||
|
||||
// Cálculos
|
||||
|
||||
/**
|
||||
* @summary Cálculo centralizado de todos los valores intermedios.
|
||||
* @returns Devuelve un objeto inmutable con todos los valores necesarios:
|
||||
@ -284,7 +268,7 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
* - totalAmount
|
||||
*
|
||||
*/
|
||||
public calculateAllAmounts() {
|
||||
public totals(): IProformaItemTotals {
|
||||
const subtotalAmount = this._calculateSubtotalAmount();
|
||||
|
||||
const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount);
|
||||
@ -320,6 +304,6 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> {
|
||||
|
||||
taxesAmount,
|
||||
totalAmount,
|
||||
} as const;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 { 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 = {
|
||||
items?: ProformaItem[];
|
||||
languageCode: LanguageCode;
|
||||
currencyCode: CurrencyCode;
|
||||
globalDiscountPercentage: Percentage;
|
||||
|
||||
// Estos campos vienen de la cabecera,
|
||||
// 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> {
|
||||
private languageCode!: LanguageCode;
|
||||
private currencyCode!: CurrencyCode;
|
||||
private globalDiscountPercentage!: Percentage;
|
||||
export interface IProformaItems extends Collection<IProformaItem> {
|
||||
valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
|
||||
totals(): IProformaItemTotals;
|
||||
|
||||
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) {
|
||||
super(props.items ?? []);
|
||||
this.languageCode = props.languageCode;
|
||||
this.currencyCode = props.currencyCode;
|
||||
this.globalDiscountPercentage = props.globalDiscountPercentage;
|
||||
|
||||
this.ensureSameCurrencyAndLanguage(this.items);
|
||||
}
|
||||
|
||||
public static create(props: ProformaItemsProps): ProformaItems {
|
||||
return new ProformaItems(props);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private _sumAmounts(selector: (item: ProformaItem) => ItemAmount): ItemAmount {
|
||||
return this.getAll().reduce(
|
||||
(acc, item) => acc.add(selector(item)),
|
||||
ItemAmount.zero(this.currencyCode.code)
|
||||
);
|
||||
public valued(): IProformaItem[] {
|
||||
return this.filter((item) => item.isValued());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @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
|
||||
* 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
|
||||
// tiene el mismo "currencyCode" y "languageCode" que la colección de items.
|
||||
const same =
|
||||
this.languageCode.equals(item.languageCode) &&
|
||||
this.currencyCode.equals(item.currencyCode) &&
|
||||
this.globalDiscountPercentage.equals(
|
||||
item.globalDiscountPercentage.match(
|
||||
(v) => v,
|
||||
() => ItemDiscountPercentage.zero()
|
||||
)
|
||||
);
|
||||
this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
|
||||
|
||||
if (!same) return false;
|
||||
|
||||
@ -92,7 +76,7 @@ export class ProformaItems extends Collection<ProformaItem> {
|
||||
* @remarks
|
||||
* 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 itemDiscountAmount = ItemAmount.zero(this.currencyCode.code);
|
||||
@ -109,7 +93,7 @@ export class ProformaItems extends Collection<ProformaItem> {
|
||||
let totalAmount = ItemAmount.zero(this.currencyCode.code);
|
||||
|
||||
for (const item of this.getAll()) {
|
||||
const amounts = item.calculateAllAmounts();
|
||||
const amounts = item.totals();
|
||||
|
||||
// Subtotales
|
||||
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount);
|
||||
@ -152,114 +136,11 @@ export class ProformaItems extends Collection<ProformaItem> {
|
||||
} as const;
|
||||
}
|
||||
|
||||
public getSubtotalAmount(): ItemAmount {
|
||||
return this.calculateAllAmounts().subtotalAmount;
|
||||
}
|
||||
|
||||
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;
|
||||
private ensureSameCurrencyAndLanguage(items: IProformaItem[]): void {
|
||||
for (const item of items) {
|
||||
if (!item.currencyCode.equals(this.currencyCode)) {
|
||||
throw new Error("[ProformaItems] All items must share the same currency.");
|
||||
}
|
||||
>();
|
||||
|
||||
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;
|
||||
})
|
||||
);*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./proforma-tax.entity";
|
||||
export * from "./proforma-taxes.collection";
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-tax-calculator";
|
||||
@ -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());
|
||||
}
|
||||
@ -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}`;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from "./proforma-item-tax-group.vo";
|
||||
export * from "./proforma-tax-group.vo";
|
||||
export * from "./proforma-item-taxes.vo";
|
||||
|
||||
@ -4,15 +4,45 @@ import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { ItemAmount } from "../../common/value-objects";
|
||||
|
||||
export interface ProformaItemTaxGroupProps {
|
||||
export type ProformaItemTaxesProps = {
|
||||
iva: Maybe<Tax>; // si existe
|
||||
rec: 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> {
|
||||
static create(props: ProformaItemTaxGroupProps) {
|
||||
return Result.ok(new ProformaItemTaxGroup(props));
|
||||
export class ProformaItemTaxes
|
||||
extends ValueObject<ProformaItemTaxesProps>
|
||||
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) {
|
||||
@ -46,37 +76,6 @@ export class ProformaItemTaxGroup extends ValueObject<ProformaItemTaxGroupProps>
|
||||
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() {
|
||||
return this.props;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -37,12 +37,12 @@ export class CustomerInvoiceItemModel extends Model<
|
||||
declare subtotal_amount_scale: number;
|
||||
|
||||
// Discount percentage
|
||||
declare discount_percentage_value: CreationOptional<number | null>;
|
||||
declare discount_percentage_scale: number;
|
||||
declare item_discount_percentage_value: CreationOptional<number | null>;
|
||||
declare item_discount_percentage_scale: number;
|
||||
|
||||
// Discount amount
|
||||
declare discount_amount_value: number;
|
||||
declare discount_amount_scale: number;
|
||||
declare item_discount_amount_value: number;
|
||||
declare item_discount_amount_scale: number;
|
||||
|
||||
// Porcentaje de descuento global proporcional a esta línea.
|
||||
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_scale: number;
|
||||
|
||||
// Retention amount
|
||||
declare retention_amount_value: number;
|
||||
declare retention_amount_scale: number;
|
||||
|
||||
@ -185,25 +184,25 @@ export default (database: Sequelize) => {
|
||||
defaultValue: 4,
|
||||
},
|
||||
|
||||
discount_percentage_value: {
|
||||
item_discount_percentage_value: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
discount_percentage_scale: {
|
||||
item_discount_percentage_scale: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: false,
|
||||
defaultValue: 2,
|
||||
},
|
||||
|
||||
discount_amount_value: {
|
||||
item_discount_amount_value: {
|
||||
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
discount_amount_scale: {
|
||||
item_discount_amount_scale: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: false,
|
||||
defaultValue: 4,
|
||||
|
||||
@ -35,7 +35,6 @@ export class CustomerInvoiceTaxModel extends Model<
|
||||
declare iva_percentage_scale: number;
|
||||
|
||||
// IVA amount
|
||||
|
||||
declare iva_amount_value: number;
|
||||
declare iva_amount_scale: number;
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ export class CustomerInvoiceModel extends Model<
|
||||
declare items_discount_amount_scale: number;
|
||||
|
||||
// Global/header discount percentage
|
||||
declare global_discount_percentage_value: number;
|
||||
declare global_discount_percentage_value: CreationOptional<number | null>;
|
||||
declare global_discount_percentage_scale: number;
|
||||
|
||||
// Global/header discount amount
|
||||
|
||||
@ -2,7 +2,6 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
LanguageCode,
|
||||
Percentage,
|
||||
TextValue,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
@ -16,6 +15,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import type { IIssuedInvoiceDomainMapper } from "../../../../../../application";
|
||||
import {
|
||||
DiscountPercentage,
|
||||
InvoiceAmount,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
@ -68,8 +68,6 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
|
||||
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
||||
|
||||
const isIssuedInvoice = Boolean(raw.is_proforma);
|
||||
|
||||
const proformaId = extractOrPushError(
|
||||
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
|
||||
"proforma_id",
|
||||
@ -181,9 +179,8 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
|
||||
// % descuento global (VO)
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
Percentage.create({
|
||||
DiscountPercentage.create({
|
||||
value: Number(raw.global_discount_percentage_value ?? 0),
|
||||
scale: Number(raw.global_discount_percentage_scale ?? 2),
|
||||
}),
|
||||
"global_discount_percentage_value",
|
||||
errors
|
||||
@ -265,7 +262,6 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
invoiceId,
|
||||
companyId,
|
||||
customerId,
|
||||
isIssuedInvoice,
|
||||
proformaId,
|
||||
status,
|
||||
series,
|
||||
@ -294,50 +290,42 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
}
|
||||
|
||||
public mapToDomain(
|
||||
source: CustomerInvoiceModel,
|
||||
raw: CustomerInvoiceModel,
|
||||
params?: MapperParamsType
|
||||
): Result<IssuedInvoice, Error> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
// 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)
|
||||
const recipientResult = this._recipientMapper.mapToDomain(source, {
|
||||
const recipientResult = this._recipientMapper.mapToDomain(raw, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 3) Verifactu (snapshot en la factura o include)
|
||||
const verifactuResult = this._verifactuMapper.mapToDomain(source.verifactu, {
|
||||
const verifactuResult = this._verifactuMapper.mapToDomain(raw.verifactu, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 4) Items (colección)
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(
|
||||
source.items,
|
||||
source.items.length,
|
||||
{
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
}
|
||||
);
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 5) Taxes (colección)
|
||||
const taxesResults = this._taxesMapper.mapToDomainCollection(
|
||||
source.taxes,
|
||||
source.taxes.length,
|
||||
{
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
}
|
||||
);
|
||||
const taxesResults = this._taxesMapper.mapToDomainCollection(raw.taxes, raw.taxes.length, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 6) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
if (errors.length > 0) {
|
||||
@ -499,7 +487,6 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
|
||||
reference: maybeToNullable(source.reference, (reference) => reference),
|
||||
description: maybeToNullable(source.description, (description) => description),
|
||||
|
||||
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
|
||||
|
||||
payment_method_id: maybeToNullable(
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type IssuedInvoice,
|
||||
IssuedInvoiceItem,
|
||||
type IssuedInvoiceItemProps,
|
||||
@ -94,25 +95,25 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
);
|
||||
|
||||
const itemDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.discount_percentage_value, (v) =>
|
||||
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
|
||||
ItemDiscountPercentage.create({ value: v })
|
||||
),
|
||||
`items[${index}].discount_percentage_value`,
|
||||
`items[${index}].item_discount_percentage_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const itemDiscountAmount = extractOrPushError(
|
||||
ItemAmount.create({
|
||||
value: raw.discount_amount_value,
|
||||
value: raw.item_discount_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`items[${index}].discount_amount_value`,
|
||||
`items[${index}].item_discount_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.global_discount_percentage_value, (v) =>
|
||||
ItemDiscountPercentage.create({ value: v })
|
||||
DiscountPercentage.create({ value: v })
|
||||
),
|
||||
`items[${index}].global_discount_percentage_value`,
|
||||
errors
|
||||
@ -357,16 +358,16 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
|
||||
subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
|
||||
|
||||
discount_percentage_value: maybeToNullable(
|
||||
item_discount_percentage_value: maybeToNullable(
|
||||
source.itemDiscountPercentage,
|
||||
(v) => v.toPrimitive().value
|
||||
),
|
||||
discount_percentage_scale:
|
||||
item_discount_percentage_scale:
|
||||
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
|
||||
ItemDiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
|
||||
discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
|
||||
item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
|
||||
item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
|
||||
|
||||
global_discount_percentage_value: maybeToNullable(
|
||||
source.globalDiscountPercentage,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
Percentage,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
@ -14,10 +15,12 @@ import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
InvoiceAmount,
|
||||
InvoiceTaxPercentage,
|
||||
type IssuedInvoice,
|
||||
type IssuedInvoiceProps,
|
||||
IssuedInvoiceTax,
|
||||
ItemAmount,
|
||||
ItemDiscountPercentage,
|
||||
TaxPercentage,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
CustomerInvoiceTaxCreationAttributes,
|
||||
@ -57,7 +60,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
}
|
||||
|
||||
public mapToDomain(
|
||||
source: CustomerInvoiceTaxModel,
|
||||
raw: CustomerInvoiceTaxModel,
|
||||
params?: MapperParamsType
|
||||
): Result<IssuedInvoiceTax, Error> {
|
||||
const { errors, index, attributes } = params as {
|
||||
@ -68,18 +71,18 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
|
||||
const taxableAmount = extractOrPushError(
|
||||
InvoiceAmount.create({
|
||||
value: source.taxable_amount_value,
|
||||
value: raw.taxable_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`taxes[${index}].taxable_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const ivaCode = source.iva_code;
|
||||
const ivaCode = raw.iva_code;
|
||||
|
||||
const ivaPercentage = extractOrPushError(
|
||||
InvoiceTaxPercentage.create({
|
||||
value: source.iva_percentage_value,
|
||||
TaxPercentage.create({
|
||||
value: raw.iva_percentage_value,
|
||||
}),
|
||||
`taxes[${index}].iva_percentage_value`,
|
||||
errors
|
||||
@ -87,52 +90,52 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
|
||||
const ivaAmount = extractOrPushError(
|
||||
InvoiceAmount.create({
|
||||
value: source.iva_amount_value,
|
||||
value: raw.iva_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`taxes[${index}].iva_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const recCode = maybeFromNullableOrEmptyString(source.rec_code);
|
||||
const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
|
||||
|
||||
const recPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(source.rec_percentage_value, (value) =>
|
||||
InvoiceTaxPercentage.create({ value })
|
||||
),
|
||||
maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
|
||||
`taxes[${index}].rec_percentage_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const recAmount = extractOrPushError(
|
||||
maybeFromNullableResult(source.rec_amount_value, (value) =>
|
||||
InvoiceAmount.create({ value, currency_code: attributes.currencyCode?.code })
|
||||
),
|
||||
InvoiceAmount.create({
|
||||
value: raw.rec_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`taxes[${index}].rec_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const retentionCode = maybeFromNullableOrEmptyString(source.retention_code);
|
||||
const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code);
|
||||
|
||||
const retentionPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(source.retention_percentage_value, (value) =>
|
||||
InvoiceTaxPercentage.create({ value })
|
||||
maybeFromNullableResult(raw.retention_percentage_value, (value) =>
|
||||
TaxPercentage.create({ value })
|
||||
),
|
||||
`taxes[${index}].retention_percentage_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const retentionAmount = extractOrPushError(
|
||||
maybeFromNullableResult(source.retention_amount_value, (value) =>
|
||||
InvoiceAmount.create({ value, currency_code: attributes.currencyCode?.code })
|
||||
),
|
||||
InvoiceAmount.create({
|
||||
value: raw.retention_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`taxes[${index}].retention_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
const taxesAmount = extractOrPushError(
|
||||
InvoiceAmount.create({
|
||||
value: source.taxes_amount_value,
|
||||
value: raw.taxes_amount_value,
|
||||
currency_code: attributes.currencyCode?.code,
|
||||
}),
|
||||
`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_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_scale: maybeToNullable(source.recAmount, (v) => v.toPrimitive().scale) ?? 4,
|
||||
rec_amount_value: source.recAmount.toPrimitive().value,
|
||||
rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
|
||||
|
||||
// RET
|
||||
retention_code: maybeToNullableString(source.retentionCode),
|
||||
@ -221,14 +225,12 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
(v) => v.toPrimitive().value
|
||||
),
|
||||
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(
|
||||
source.retentionAmount,
|
||||
(v) => v.toPrimitive().value
|
||||
),
|
||||
retention_amount_value: source.retentionAmount.toPrimitive().value,
|
||||
retention_amount_scale:
|
||||
maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4,
|
||||
source.retentionAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
|
||||
|
||||
// TOTAL
|
||||
taxes_amount_value: source.taxesAmount.value,
|
||||
|
||||
@ -2,7 +2,6 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
LanguageCode,
|
||||
Percentage,
|
||||
TextValue,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
@ -16,6 +15,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import type { IProformaDomainMapper } from "../../../../../../application";
|
||||
import {
|
||||
DiscountPercentage,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
@ -46,86 +46,80 @@ export class SequelizeProformaDomainMapper
|
||||
|
||||
this._itemsMapper = new SequelizeProformaItemDomainMapper(params);
|
||||
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 {
|
||||
errors: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
|
||||
const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors);
|
||||
const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors);
|
||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||
|
||||
const customerId = extractOrPushError(
|
||||
UniqueID.create(source.customer_id),
|
||||
"customer_id",
|
||||
errors
|
||||
);
|
||||
|
||||
const isProforma = Boolean(source.is_proforma);
|
||||
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
||||
|
||||
const proformaId = extractOrPushError(
|
||||
maybeFromNullableResult(source.proforma_id, (v) => UniqueID.create(v)),
|
||||
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
|
||||
"proforma_id",
|
||||
errors
|
||||
);
|
||||
|
||||
const status = extractOrPushError(InvoiceStatus.create(source.status), "status", errors);
|
||||
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
|
||||
|
||||
const series = extractOrPushError(
|
||||
maybeFromNullableResult(source.series, (v) => InvoiceSerie.create(v)),
|
||||
maybeFromNullableResult(raw.series, (v) => InvoiceSerie.create(v)),
|
||||
"series",
|
||||
errors
|
||||
);
|
||||
|
||||
const invoiceNumber = extractOrPushError(
|
||||
InvoiceNumber.create(source.invoice_number),
|
||||
InvoiceNumber.create(raw.invoice_number),
|
||||
"invoice_number",
|
||||
errors
|
||||
);
|
||||
|
||||
// Fechas
|
||||
const invoiceDate = extractOrPushError(
|
||||
UtcDate.createFromISO(source.invoice_date),
|
||||
UtcDate.createFromISO(raw.invoice_date),
|
||||
"invoice_date",
|
||||
errors
|
||||
);
|
||||
|
||||
const operationDate = extractOrPushError(
|
||||
maybeFromNullableResult(source.operation_date, (v) => UtcDate.createFromISO(v)),
|
||||
maybeFromNullableResult(raw.operation_date, (v) => UtcDate.createFromISO(v)),
|
||||
"operation_date",
|
||||
errors
|
||||
);
|
||||
|
||||
// Idioma / divisa
|
||||
const languageCode = extractOrPushError(
|
||||
LanguageCode.create(source.language_code),
|
||||
LanguageCode.create(raw.language_code),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(source.currency_code),
|
||||
CurrencyCode.create(raw.currency_code),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
|
||||
// Textos opcionales
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(source.reference, (value) => Result.ok(String(value))),
|
||||
maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
|
||||
const description = extractOrPushError(
|
||||
maybeFromNullableResult(source.description, (value) => Result.ok(String(value))),
|
||||
maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))),
|
||||
"description",
|
||||
errors
|
||||
);
|
||||
|
||||
const notes = extractOrPushError(
|
||||
maybeFromNullableResult(source.notes, (value) => TextValue.create(value)),
|
||||
maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)),
|
||||
"notes",
|
||||
errors
|
||||
);
|
||||
@ -133,16 +127,16 @@ export class SequelizeProformaDomainMapper
|
||||
// Método de pago (VO opcional con id + descripción)
|
||||
let paymentMethod = Maybe.none<InvoicePaymentMethod>();
|
||||
|
||||
if (!isNullishOrEmpty(source.payment_method_id)) {
|
||||
if (!isNullishOrEmpty(raw.payment_method_id)) {
|
||||
const paymentId = extractOrPushError(
|
||||
UniqueID.create(String(source.payment_method_id)),
|
||||
UniqueID.create(String(raw.payment_method_id)),
|
||||
"paymentMethod.id",
|
||||
errors
|
||||
);
|
||||
|
||||
const paymentVO = extractOrPushError(
|
||||
InvoicePaymentMethod.create(
|
||||
{ paymentDescription: String(source.payment_method_description ?? "") },
|
||||
{ paymentDescription: String(raw.payment_method_description ?? "") },
|
||||
paymentId ?? undefined
|
||||
),
|
||||
"payment_method_description",
|
||||
@ -154,13 +148,12 @@ export class SequelizeProformaDomainMapper
|
||||
}
|
||||
}
|
||||
|
||||
// % descuento (VO)
|
||||
const discountPercentage = extractOrPushError(
|
||||
Percentage.create({
|
||||
value: Number(source.discount_percentage_value ?? 0),
|
||||
scale: Number(source.discount_percentage_scale ?? 2),
|
||||
// % descuento global (VO)
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
DiscountPercentage.create({
|
||||
value: Number(raw.global_discount_percentage_value ?? 0),
|
||||
}),
|
||||
"discount_percentage_value",
|
||||
"global_discount_percentage_value",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -168,7 +161,6 @@ export class SequelizeProformaDomainMapper
|
||||
invoiceId,
|
||||
companyId,
|
||||
customerId,
|
||||
isProforma,
|
||||
proformaId,
|
||||
status,
|
||||
series,
|
||||
@ -180,38 +172,35 @@ export class SequelizeProformaDomainMapper
|
||||
notes,
|
||||
languageCode,
|
||||
currencyCode,
|
||||
discountPercentage,
|
||||
paymentMethod,
|
||||
|
||||
globalDiscountPercentage,
|
||||
};
|
||||
}
|
||||
|
||||
public mapToDomain(
|
||||
source: CustomerInvoiceModel,
|
||||
raw: CustomerInvoiceModel,
|
||||
params?: MapperParamsType
|
||||
): Result<Proforma, Error> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
// 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)
|
||||
const recipientResult = this._recipientMapper.mapToDomain(source, {
|
||||
const recipientResult = this._recipientMapper.mapToDomain(raw, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 3) Items (colección)
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(
|
||||
source.items,
|
||||
source.items.length,
|
||||
{
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
}
|
||||
);
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
|
||||
errors,
|
||||
attributes,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 4) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
if (errors.length > 0) {
|
||||
@ -225,14 +214,13 @@ export class SequelizeProformaDomainMapper
|
||||
const items = ProformaItems.create({
|
||||
languageCode: attributes.languageCode!,
|
||||
currencyCode: attributes.currencyCode!,
|
||||
globalDiscountPercentage: attributes.discountPercentage!,
|
||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||
items: itemsResults.data.getAll(),
|
||||
});
|
||||
|
||||
const invoiceProps: ProformaProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
isProforma: attributes.isProforma,
|
||||
status: attributes.status!,
|
||||
series: attributes.series!,
|
||||
invoiceNumber: attributes.invoiceNumber!,
|
||||
@ -249,7 +237,7 @@ export class SequelizeProformaDomainMapper
|
||||
languageCode: attributes.languageCode!,
|
||||
currencyCode: attributes.currencyCode!,
|
||||
|
||||
globalDiscountPercentage: attributes.discountPercentage!,
|
||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||
|
||||
paymentMethod: attributes.paymentMethod!,
|
||||
|
||||
@ -331,11 +319,12 @@ export class SequelizeProformaDomainMapper
|
||||
company_id: source.companyId.toPrimitive(),
|
||||
|
||||
// Flags / estado / serie / número
|
||||
is_proforma: source.isProforma,
|
||||
is_proforma: true,
|
||||
status: source.status.toPrimitive(),
|
||||
proforma_id: null,
|
||||
|
||||
series: maybeToNullable(source.series, (v) => v.toPrimitive()),
|
||||
invoice_number: source.invoiceNumber.toPrimitive(),
|
||||
|
||||
invoice_date: source.invoiceDate.toPrimitive(),
|
||||
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
|
||||
language_code: source.languageCode.toPrimitive(),
|
||||
@ -345,6 +334,15 @@ export class SequelizeProformaDomainMapper
|
||||
description: maybeToNullable(source.description, (description) => description),
|
||||
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_scale: allAmounts.subtotalAmount.scale,
|
||||
|
||||
@ -369,15 +367,6 @@ export class SequelizeProformaDomainMapper
|
||||
total_amount_value: allAmounts.totalAmount.value,
|
||||
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(),
|
||||
...recipient,
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
@ -25,6 +26,21 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
|
||||
CustomerInvoiceTaxCreationAttributes,
|
||||
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(
|
||||
source: CustomerInvoiceTaxModel,
|
||||
params?: MapperParamsType
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
"noFallthroughCasesInSwitch": 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"]
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
import { translateZodValidationError } from "../helpers";
|
||||
|
||||
import { ValueObject } from "./value-object";
|
||||
|
||||
interface TaxCodeProps {
|
||||
@ -28,7 +27,8 @@ export class TaxCode extends ValueObject<TaxCodeProps> {
|
||||
}
|
||||
|
||||
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) {
|
||||
return Result.fail(
|
||||
@ -36,7 +36,7 @@ export class TaxCode extends ValueObject<TaxCodeProps> {
|
||||
);
|
||||
}
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
return Result.ok(new TaxCode({ value: valueIsValid.data! }));
|
||||
return Result.ok(new TaxCode({ value: valueIsValid.data! }));*/
|
||||
}
|
||||
|
||||
getProps(): string {
|
||||
|
||||
@ -31,7 +31,7 @@ export class Collection<T> {
|
||||
addCollection(collection: Collection<T>): boolean {
|
||||
this.items.push(...collection.items);
|
||||
if (this.totalItems !== null) {
|
||||
this.totalItems = this.totalItems + collection.totalItems;
|
||||
this.totalItems += collection.totalItems;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
7
uecko-erp.code-workspace
Normal file
7
uecko-erp.code-workspace
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user