This commit is contained in:
David Arranz 2026-02-22 19:15:22 +01:00
parent 167eaff171
commit 056e78548e
49 changed files with 296 additions and 362 deletions

View File

@ -36,11 +36,6 @@ export class Tax extends ValueObject<TaxProps> {
.int()
.min(Tax.MIN_VALUE, "La tasa de impuesto no puede ser negativa.")
.max(Tax.MAX_VALUE * 10 ** Tax.MAX_SCALE, "La tasa de impuesto es demasiado alta."),
scale: z
.number()
.int()
.min(Tax.MIN_SCALE)
.max(Tax.MAX_SCALE, `La escala debe estar entre ${Tax.MIN_SCALE} y ${Tax.MAX_SCALE}.`),
name: z
.string()
.min(1, "El nombre del impuesto es obligatorio.")

View File

@ -32,8 +32,10 @@ export interface IIssuedInvoiceFullSnapshot {
subtotal_amount: { value: string; scale: string; currency_code: string };
items_discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string };
total_discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };

View File

@ -9,8 +9,8 @@ export interface IIssuedInvoiceItemFullSnapshot {
subtotal_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string };
item_discount_percentage: { value: string; scale: string };
item_discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string };

View File

@ -17,7 +17,8 @@ export class IssuedInvoiceItemsFullSnapshotBuilder
implements IIssuedInvoiceItemsFullSnapshotBuilder
{
private mapItem(invoiceItem: IssuedInvoiceItem, index: number): IIssuedInvoiceItemFullSnapshot {
const isValued = invoiceItem.isValued;
const isValued = invoiceItem.isValued();
return {
id: invoiceItem.id.toPrimitive(),
is_valued: String(isValued),
@ -32,14 +33,15 @@ export class IssuedInvoiceItemsFullSnapshotBuilder
? invoiceItem.subtotalAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
discount_percentage: maybeToEmptyPercentageObjectString(invoiceItem.itemDiscountPercentage),
discount_amount: isValued
item_discount_percentage: maybeToEmptyPercentageObjectString(
invoiceItem.itemDiscountPercentage
),
item_discount_amount: isValued
? invoiceItem.itemDiscountAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
global_discount_percentage: maybeToEmptyPercentageObjectString(
invoiceItem.globalDiscountPercentage
),
global_discount_percentage: invoiceItem.globalDiscountPercentage.toObjectString(),
global_discount_amount: isValued
? invoiceItem.globalDiscountAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,

View File

@ -1,9 +1,5 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import {
maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd";
import type { IssuedInvoiceTax, IssuedInvoiceTaxes } from "../../../../domain";
@ -25,11 +21,11 @@ export class IssuedInvoiceTaxesFullSnapshotBuilder
rec_code: maybeToEmptyString(invoiceTax.recCode),
rec_percentage: maybeToEmptyPercentageObjectString(invoiceTax.recPercentage),
rec_amount: maybeToEmptyMoneyObjectString(invoiceTax.recAmount),
rec_amount: invoiceTax.recAmount.toObjectString(),
retention_code: maybeToEmptyString(invoiceTax.retentionCode),
retention_percentage: maybeToEmptyPercentageObjectString(invoiceTax.retentionPercentage),
retention_amount: maybeToEmptyMoneyObjectString(invoiceTax.retentionAmount),
retention_amount: invoiceTax.retentionAmount.toObjectString(),
taxes_amount: invoiceTax.taxesAmount.toObjectString(),
};

View File

@ -8,14 +8,21 @@ import {
ProformaRecipientFullSnapshotBuilder,
ProformaReportSnapshotBuilder,
ProformaTaxReportSnapshotBuilder,
ProformaTaxesFullSnapshotBuilder,
} from "../snapshot-builders";
export function buildProformaSnapshotBuilders() {
const itemsBuilder = new ProformaItemsFullSnapshotBuilder();
const taxesBuilder = new ProformaTaxesFullSnapshotBuilder();
const recipientBuilder = new ProformaRecipientFullSnapshotBuilder();
const fullSnapshotBuilder = new ProformaFullSnapshotBuilder(itemsBuilder, recipientBuilder);
const fullSnapshotBuilder = new ProformaFullSnapshotBuilder(
itemsBuilder,
recipientBuilder,
taxesBuilder
);
const listSnapshotBuilder = new ProformaListItemSnapshotBuilder();

View File

@ -1,11 +1,12 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import { InvoiceAmount, type Proforma } from "../../../../domain";
import type { Proforma } from "../../../../domain";
import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface";
import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder";
import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder";
import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-snapshot-builder";
export interface IProformaFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaFullSnapshot> {}
@ -13,14 +14,14 @@ export interface IProformaFullSnapshotBuilder
export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder {
constructor(
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder,
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
) {}
toOutput(invoice: Proforma): IProformaFullSnapshot {
const items = this.itemsBuilder.toOutput(invoice.items);
const recipient = this.recipientBuilder.toOutput(invoice);
const allAmounts = invoice.calculateAllAmounts();
const taxes = this.taxesBuilder.toOutput(invoice.taxes());
const payment = invoice.paymentMethod.match(
(payment) => {
@ -33,51 +34,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
() => undefined
);
let totalIvaAmount = InvoiceAmount.zero(invoice.currencyCode.code);
let totalRecAmount = InvoiceAmount.zero(invoice.currencyCode.code);
let totalRetentionAmount = InvoiceAmount.zero(invoice.currencyCode.code);
const invoiceTaxes = invoice.getTaxes().map((taxGroup) => {
const { ivaAmount, recAmount, retentionAmount, totalAmount } = taxGroup.calculateAmounts();
totalIvaAmount = totalIvaAmount.add(ivaAmount);
totalRecAmount = totalRecAmount.add(recAmount);
totalRetentionAmount = totalRetentionAmount.add(retentionAmount);
return {
taxable_amount: taxGroup.taxableAmount.toObjectString(),
iva_code: taxGroup.iva.code,
iva_percentage: taxGroup.iva.percentage.toObjectString(),
iva_amount: ivaAmount.toObjectString(),
rec_code: taxGroup.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: taxGroup.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: recAmount.toObjectString(),
retention_code: taxGroup.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: taxGroup.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: retentionAmount.toObjectString(),
taxes_amount: totalAmount.toObjectString(),
};
});
const allTotals = invoice.totals();
return {
id: invoice.id.toString(),
@ -102,22 +59,24 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
payment_method: payment,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
items_discount_amount: allTotals.itemDiscountAmount.toObjectString(),
discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
global_discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
global_discount_amount: allTotals.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
total_discount_amount: allTotals.totalDiscountAmount.toObjectString(),
iva_amount: totalIvaAmount.toObjectString(),
rec_amount: totalRecAmount.toObjectString(),
retention_amount: totalRetentionAmount.toObjectString(),
taxable_amount: allTotals.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
iva_amount: allTotals.ivaAmount.toObjectString(),
rec_amount: allTotals.recAmount.toObjectString(),
retention_amount: allTotals.retentionAmount.toObjectString(),
taxes: invoiceTaxes,
taxes_amount: allTotals.taxesAmount.toObjectString(),
total_amount: allTotals.totalAmount.toObjectString(),
taxes,
items,

View File

@ -1,5 +1,6 @@
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
export interface IProformaFullSnapshot {
id: string;
@ -30,8 +31,10 @@ export interface IProformaFullSnapshot {
subtotal_amount: { value: string; scale: string; currency_code: string };
items_discount_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string };
total_discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
@ -42,23 +45,7 @@ export interface IProformaFullSnapshot {
taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string };
taxes: Array<{
taxable_amount: { value: string; scale: string; currency_code: string };
iva_code: string;
iva_percentage: { value: string; scale: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_code: string;
rec_percentage: { value: string; scale: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_code: string;
retention_percentage: { value: string; scale: string };
retention_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
}>;
taxes: IProformaTaxFullSnapshot[];
items: IProformaItemFullSnapshot[];

View File

@ -15,6 +15,8 @@ export interface IProformaItemFullSnapshot {
global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string };
total_discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
iva_code: string;

View File

@ -5,7 +5,6 @@ import {
maybeToEmptyQuantityObjectString,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain";
@ -16,10 +15,8 @@ export interface IProformaItemsFullSnapshotBuilder
export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder {
private mapItem(proformaItem: ProformaItem, index: number): IProformaItemFullSnapshot {
const allAmounts = proformaItem.calculateAllAmounts();
const isValued = proformaItem.isValued;
const noneIfNotValued = <T>(value: Maybe<T>): Maybe<T> => (isValued ? value : Maybe.none<T>());
const allAmounts = proformaItem.totals();
const isValued = proformaItem.isValued();
return {
id: proformaItem.id.toPrimitive(),
@ -40,10 +37,11 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
? allAmounts.itemDiscountAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
global_discount_percentage: maybeToEmptyPercentageObjectString(
proformaItem.globalDiscountPercentage
),
global_discount_amount: maybeToEmptyMoneyObjectString(proformaItem.globalDiscountAmount),
global_discount_percentage: proformaItem.globalDiscountPercentage.toObjectString(),
global_discount_amount: isValued
? allAmounts.globalDiscountAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
total_discount_amount: isValued
? allAmounts.totalDiscountAmount.toObjectString()
@ -53,20 +51,26 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
? allAmounts.taxableAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
iva_code: maybeToEmptyString(proformaItem.ivaCode),
iva_percentage: maybeToEmptyPercentageObjectString(proformaItem.ivaPercentage),
iva_amount: maybeToEmptyMoneyObjectString(proformaItem.ivaAmount),
iva_code: maybeToEmptyString(proformaItem.ivaCode()),
iva_percentage: maybeToEmptyPercentageObjectString(proformaItem.ivaPercentage()),
iva_amount: isValued ? allAmounts.ivaAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT,
rec_code: maybeToEmptyString(proformaItem.recCode),
rec_percentage: maybeToEmptyPercentageObjectString(proformaItem.recPercentage),
rec_amount: maybeToEmptyMoneyObjectString(proformaItem.recAmount),
rec_code: maybeToEmptyString(proformaItem.recCode()),
rec_percentage: maybeToEmptyPercentageObjectString(proformaItem.recPercentage()),
rec_amount: isValued ? allAmounts.recAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT,
retention_code: maybeToEmptyString(proformaItem.retentionCode),
retention_percentage: maybeToEmptyPercentageObjectString(proformaItem.retentionPercentage),
retention_amount: maybeToEmptyMoneyObjectString(proformaItem.retentionAmount),
retention_code: maybeToEmptyString(proformaItem.retentionCode()),
retention_percentage: maybeToEmptyPercentageObjectString(proformaItem.retentionPercentage()),
retention_amount: isValued
? allAmounts.retentionAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
taxes_amount: maybeToEmptyMoneyObjectString(proformaItem.taxesAmount),
total_amount: maybeToEmptyMoneyObjectString(proformaItem.totalAmount),
taxes_amount: isValued
? allAmounts.taxesAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
total_amount: isValued
? allAmounts.totalAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT,
};
}

View File

@ -1,39 +1,36 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import {
maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd";
import type { Collection } from "@repo/rdx-utils";
import type { ProformaTax, ProformaTaxes } from "../../../../domain";
import type { IProformaTaxTotals } from "../../../../domain";
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
export interface IProformaTaxesFullSnapshotBuilder
extends ISnapshotBuilder<ProformaTaxes, IProformaTaxFullSnapshot[]> {}
extends ISnapshotBuilder<Collection<IProformaTaxTotals>, IProformaTaxFullSnapshot[]> {}
export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnapshotBuilder {
private mapItem(proformaTax: ProformaTax, index: number): IProformaTaxFullSnapshot {
private mapItem(proformaTax: IProformaTaxTotals, index: number): IProformaTaxFullSnapshot {
return {
taxable_amount: proformaTax.taxableAmount.toObjectString(),
iva_code: proformaTax.ivaCode.toString(),
iva_percentage: proformaTax.ivaPercentage.toObjectString(),
iva_code: maybeToEmptyString(proformaTax.ivaCode),
iva_percentage: maybeToEmptyPercentageObjectString(proformaTax.ivaPercentage),
iva_amount: proformaTax.ivaAmount.toObjectString(),
rec_code: maybeToEmptyString(proformaTax.recCode),
rec_percentage: maybeToEmptyPercentageObjectString(proformaTax.recPercentage),
rec_amount: maybeToEmptyMoneyObjectString(proformaTax.recAmount),
rec_amount: proformaTax.recAmount.toObjectString(),
retention_code: maybeToEmptyString(proformaTax.retentionCode),
retention_percentage: maybeToEmptyPercentageObjectString(proformaTax.retentionPercentage),
retention_amount: maybeToEmptyMoneyObjectString(proformaTax.retentionAmount),
retention_amount: proformaTax.retentionAmount.toObjectString(),
taxes_amount: proformaTax.taxesAmount.toObjectString(),
};
}
toOutput(invoiceTaxes: ProformaTaxes): IProformaTaxFullSnapshot[] {
toOutput(invoiceTaxes: Collection<IProformaTaxTotals>): IProformaTaxFullSnapshot[] {
return invoiceTaxes.map((item, index) => this.mapItem(item, index));
}
}

View File

@ -25,7 +25,7 @@ export type IssuedInvoiceItemProps = {
itemDiscountPercentage: Maybe<DiscountPercentage>;
itemDiscountAmount: ItemAmount;
globalDiscountPercentage: Maybe<DiscountPercentage>;
globalDiscountPercentage: DiscountPercentage;
globalDiscountAmount: ItemAmount;
totalDiscountAmount: ItemAmount;
@ -51,7 +51,14 @@ export type IssuedInvoiceItemProps = {
currencyCode: CurrencyCode;
};
export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
export interface IIssuedInvoiceItem extends IssuedInvoiceItemProps {
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
}
export class IssuedInvoiceItem
extends DomainEntity<IssuedInvoiceItemProps>
implements IIssuedInvoiceItem
{
public static create(
props: IssuedInvoiceItemProps,
id?: UniqueID
@ -70,11 +77,6 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
}
// Getters
get isValued(): boolean {
return this.quantity.isSome() || this.unitAmount.isSome();
}
get description() {
return this.props.description;
}
@ -168,4 +170,8 @@ export class IssuedInvoiceItem extends DomainEntity<IssuedInvoiceItemProps> {
toPrimitive() {
return this.getProps();
}
public isValued(): boolean {
return this.props.quantity.isSome() || this.props.unitAmount.isSome();
}
}

View File

@ -1,8 +1,6 @@
import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { ItemDiscountPercentage } from "../../../common";
import type { IssuedInvoiceItem } from "./issued-invoice-item.entity";
export type IssuedInvoiceItemsProps = {
@ -44,16 +42,10 @@ export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> {
!(
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)
)
) {
)
return false;
}
return super.add(item);
}

View File

@ -284,7 +284,7 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> implements IPr
const taxableAmount = subtotalAmount.subtract(totalDiscountAmount);
// Calcular impuestos individuales a partir de la base imponible
const { ivaAmount, recAmount, retentionAmount } = this.taxes.calculateAmounts(taxableAmount);
const { ivaAmount, recAmount, retentionAmount } = this.taxes.totals(taxableAmount);
const taxesAmount = ivaAmount.add(recAmount).add(retentionAmount);
const totalAmount = taxableAmount.add(taxesAmount);

View File

@ -1,3 +1,5 @@
export * from "./aggregates";
export * from "./entities";
export * from "./errors";
export * from "./services";
export * from "./value-objects";

View File

@ -10,10 +10,10 @@ import type { IProformaTaxTotals } from "./proforma-taxes-calculator";
* 3) RET (None primero) (code, %)
*/
export function proformaCompareTaxTotals(a: IProformaTaxTotals, b: IProformaTaxTotals): number {
const byIvaCode = compareCode(a.ivaCode, b.ivaCode);
const byIvaCode = compareMaybeCodeNoneFirst(a.ivaCode, b.ivaCode);
if (byIvaCode !== 0) return byIvaCode;
const byIvaPct = comparePct(a.ivaPercentage, b.ivaPercentage);
const byIvaPct = compareMaybePctNoneFirst(a.ivaPercentage, b.ivaPercentage);
if (byIvaPct !== 0) return byIvaPct;
const byRecCode = compareMaybeCodeNoneFirst(a.recCode, b.recCode);

View File

@ -1,4 +1,4 @@
import type { TaxPercentage } from "@erp/core/api";
import type { Tax, TaxPercentage } from "@erp/core/api";
import { Maybe } from "@repo/rdx-utils";
import { type InvoiceAmount, ItemAmount } from "../../common";
@ -7,8 +7,8 @@ import type { IProformaItems } from "../entities";
type TaxGroupState = {
taxableAmount: ItemAmount;
ivaCode: string;
ivaPercentage: TaxPercentage;
ivaCode: Maybe<string>;
ivaPercentage: Maybe<TaxPercentage>;
ivaAmount: ItemAmount;
recCode: Maybe<string>;
@ -33,7 +33,7 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
const currency = items.currencyCode;
for (const item of items.valued()) {
const iva = item.taxes.iva.unwrap(); // siempre existe
const iva = item.taxes.iva;
const rec = item.taxes.rec;
const retention = item.taxes.retention;
@ -43,8 +43,8 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
map.set(key, {
taxableAmount: ItemAmount.zero(currency.code),
ivaCode: iva.code,
ivaPercentage: iva.percentage,
ivaCode: iva.isSome() ? Maybe.some(iva.unwrap().code) : Maybe.none(),
ivaPercentage: iva.isSome() ? Maybe.some(iva.unwrap().percentage) : Maybe.none(),
ivaAmount: ItemAmount.zero(currency.code),
recCode: rec.isSome() ? Maybe.some(rec.unwrap().code) : Maybe.none(),
@ -71,12 +71,10 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
return map;
}
function buildTaxGroupKey(iva: any, rec: any, retention: any): string {
const recPart = rec.isSome() ? `${rec.unwrap().code}-${rec.unwrap().percentage.value}` : "NULL";
function buildTaxGroupKey(iva: Maybe<Tax>, rec: Maybe<Tax>, retention: Maybe<Tax>): string {
const ivaPart = iva.isSome() ? `${iva.unwrap().code}` : "NULL";
const recPart = rec.isSome() ? `${rec.unwrap().code}` : "NULL";
const retentionPart = retention.isSome() ? `${retention.unwrap().code}` : "NULL";
const retentionPart = retention.isSome()
? `${retention.unwrap().code}-${retention.unwrap().percentage.value}`
: "NULL";
return `${iva.code}-${iva.percentage.value}|${recPart}|${retentionPart}`;
return `${ivaPart}|${recPart}|${retentionPart}`;
}

View File

@ -10,8 +10,8 @@ import { proformaComputeTaxGroups } from "./proforma-compute-tax-groups";
export interface IProformaTaxTotals {
taxableAmount: InvoiceAmount;
ivaCode: string;
ivaPercentage: TaxPercentage;
ivaCode: Maybe<string>;
ivaPercentage: Maybe<TaxPercentage>;
ivaAmount: InvoiceAmount;
recCode: Maybe<string>;

View File

@ -16,6 +16,12 @@ export interface IProformaItemTaxes {
retention: Maybe<Tax>; // si existe
toKey(): string; // Clave para representar un trío.
totals(taxableAmount: ItemAmount): {
ivaAmount: ItemAmount;
recAmount: ItemAmount;
retentionAmount: ItemAmount;
};
}
export class ProformaItemTaxes
@ -45,7 +51,7 @@ export class ProformaItemTaxes
return `${ivaCode};${recCode};${retentionCode}`;
}
calculateAmounts(taxableAmount: ItemAmount) {
totals(taxableAmount: ItemAmount) {
const ivaAmount = this.props.iva.match(
(iva) => taxableAmount.percentage(iva.percentage),
() => ItemAmount.zero(taxableAmount.currencyCode)

View File

@ -2,11 +2,14 @@ import type { IModuleServer } from "@erp/core/api";
import {
type IssuedInvoicesInternalDeps,
buildIssuedInvoiceServices,
buildIssuedInvoicesDependencies,
buildProformaServices,
buildProformasDependencies,
models,
} from "./infrastructure";
import { issuedInvoicesRouter } from "./infrastructure/express";
import { proformasRouter } from './infrastructure/express';
export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices",
@ -25,11 +28,11 @@ export const customerInvoicesAPIModule: IModuleServer = {
// 1) Dominio interno
const issuedInvoicesInternalDeps = buildIssuedInvoicesDependencies(params);
//const proformasInternalDeps = buildProformasDependencies(params);
const proformasInternalDeps = buildProformasDependencies(params);
// 2) Servicios públicos (Application Services)
const issuedInvoicesServices = buildProformaServices(issuedInvoicesInternalDeps);
//const proformasServices = buildProformasServices(proformasInternalDeps);
const issuedInvoicesServices = buildIssuedInvoiceServices(issuedInvoicesInternalDeps);
const proformasServices = buildProformaServices(proformasInternalDeps);
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
@ -40,13 +43,13 @@ export const customerInvoicesAPIModule: IModuleServer = {
// Servicios expuestos a otros módulos
services: {
issuedInvoices: issuedInvoicesServices,
//proformas: proformasServices
proformas: proformasServices
},
// Implementación privada del módulo
internal: {
issuedInvoices: issuedInvoicesInternalDeps,
//proformas: proformasInternalDeps
proformas: proformasInternalDeps
},
};
},
@ -66,11 +69,12 @@ export const customerInvoicesAPIModule: IModuleServer = {
"customer-invoices",
"issuedInvoices"
);
//const proformasInternalDeps = getInternal("customer-invoices", "proformas");
const proformasInternalDeps = getInternal("customer-invoices", "proformas");
// Registro de rutas HTTP
issuedInvoicesRouter(params, issuedInvoicesInternalDeps);
//proformasRouter(params, proformasInternalDeps);
proformasRouter(params, proformasInternalDeps);
logger.info("🚀 CustomerInvoices module started", {
label: this.name,

View File

@ -45,7 +45,7 @@ export class CustomerInvoiceItemModel extends Model<
declare item_discount_amount_scale: number;
// Porcentaje de descuento global proporcional a esta línea.
declare global_discount_percentage_value: CreationOptional<number | null>;
declare global_discount_percentage_value: number;
declare global_discount_percentage_scale: number;
// Importe del descuento global para esta línea
@ -210,8 +210,8 @@ export default (database: Sequelize) => {
global_discount_percentage_value: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
global_discount_percentage_scale: {

View File

@ -29,9 +29,9 @@ export class CustomerInvoiceTaxModel extends Model<
// Código de impuestos
// IVA percentage
declare iva_code: string;
declare iva_code: CreationOptional<string | null>;
declare iva_percentage_value: number;
declare iva_percentage_value: CreationOptional<number | null>;
declare iva_percentage_scale: number;
// IVA amount
@ -124,6 +124,7 @@ export default (database: Sequelize) => {
allowNull: true,
defaultValue: null,
},
iva_percentage_value: {
type: DataTypes.SMALLINT,
allowNull: true,

View File

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

View File

@ -1,2 +1,2 @@
export * from "./issued-invoices";
//export * from "./proformas";
export * from "./proformas";

View File

@ -1,3 +1,2 @@
export * from "../../issued-invoices/express";
export * from "./issued-invoices.routes";
export * from "../../issued-invoices/express/controllers";
export * from "../../issued-invoices/express/issued-invoices.routes";

View File

@ -1,4 +1,4 @@
export * from "../../proformas/express";
export * from "../../proformas/express/controllers";
export * from "./proformas.routes";
export * from "./proformas-api-error-mapper";

View File

@ -2,35 +2,18 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
import { GetProformaByIdRequestSchema, ListProformasRequestSchema } from "../../../../common";
import {
ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema,
CreateProformaRequestSchema,
DeleteProformaByIdParamsRequestSchema,
GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
UpdateProformaByIdParamsRequestSchema,
UpdateProformaByIdRequestSchema,
} from "../../../../common";
import type { IssuedInvoicesInternalDeps } from "../../issued-invoices/di";
import {
ChangeStatusProformaController,
CreateProformaController,
DeleteProformaController,
GetProformaController,
IssueProformaController as IssuedProformaController,
ListProformasController,
ReportProformaController,
UpdateProformaController,
} from "../../proformas/express";
type ProformasInternalDeps,
} from "../../proformas";
export const proformasRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => {
const { app, config } = params;
const router: Router = Router({ mergeParams: true });
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router
router.use(
@ -54,7 +37,7 @@ export const proformasRouter = (params: ModuleParams, deps: IssuedInvoicesIntern
//checkTabContext,
validateRequest(ListProformasRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.listIssuedInvoices();
const useCase = deps.useCases.listProformas();
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
}
@ -65,13 +48,13 @@ export const proformasRouter = (params: ModuleParams, deps: IssuedInvoicesIntern
//checkTabContext,
validateRequest(GetProformaByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.getIssuedInvoiceById();
const useCase = deps.useCases.getProformaById();
const controller = new GetProformaController(useCase);
return controller.execute(req, res, next);
}
);
router.post(
/*router.post(
"/",
//checkTabContext,
@ -145,7 +128,7 @@ export const proformasRouter = (params: ModuleParams, deps: IssuedInvoicesIntern
const controller = new IssuedProformaController(useCase);
return controller.execute(req, res, next);
}
);
);*/
app.use(`${config.server.apiBasePath}/proformas`, router);
};

View File

@ -5,9 +5,9 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import { GetIssuedInvoiceByIdResponseSchema } from "../../../../common/index.ts";
import type { GetIssuedInvoiceByIdUseCase } from "../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common/index.ts";
import type { GetIssuedInvoiceByIdUseCase } from "../../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class GetIssuedInvoiceByIdController extends ExpressController {
public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) {

View File

@ -0,0 +1,3 @@
export * from "./get-issued-invoice-by-id.controller";
export * from "./list-issued-invoices.controller";
export * from "./report-issued-invoice.controller";

View File

@ -6,9 +6,9 @@ import {
} from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { ListIssuedInvoicesResponseSchema } from "../../../../common/index.ts";
import type { ListIssuedInvoicesUseCase } from "../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import { ListIssuedInvoicesResponseSchema } from "../../../../../common/index.ts";
import type { ListIssuedInvoicesUseCase } from "../../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class ListIssuedInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListIssuedInvoicesUseCase) {

View File

@ -7,8 +7,8 @@ import {
} from "@erp/core/api";
import type { ReportIssueInvoiceByIdQueryRequestDTO } from "@erp/customer-invoices/common";
import type { ReportIssuedInvoiceUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class ReportIssuedInvoiceController extends ExpressController {
public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) {

View File

@ -1,3 +1,2 @@
export * from "./get-issued-invoice-by-id.controller";
export * from "./list-issued-invoices.controller";
export * from "./report-issued-invoice.controller";
export * from "./controllers";
export * from "./issued-invoices.routes";

View File

@ -7,18 +7,20 @@ import {
ListIssuedInvoicesRequestSchema,
ReportIssueInvoiceByIdParamsRequestSchema,
ReportIssueInvoiceByIdQueryRequestSchema,
} from "../../../../common/dto";
} from "../../../../common";
import type { IssuedInvoicesInternalDeps } from "../../issued-invoices/di";
import {
GetIssuedInvoiceByIdController,
ListIssuedInvoicesController,
ReportIssuedInvoiceController,
} from "../../issued-invoices";
import type { IssuedInvoicesInternalDeps } from "../../issued-invoices/di";
import { ListIssuedInvoicesController } from "../../issued-invoices/express/list-issued-invoices.controller";
} from "./controllers";
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
const { app, config } = params;
const router: Router = Router({ mergeParams: true });
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router
router.use(

View File

@ -29,7 +29,7 @@ export type ProformasInternalDeps = {
};
export function buildProformasDependencies(params: ModuleParams): ProformasInternalDeps {
const { database, env } = params;
const { database } = params;
// Infrastructure
const transactionManager = buildTransactionManager(database);
@ -37,10 +37,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
//const numberService = buildProformaNumberGenerator();
// Application helpers
const finder = buildProformaFinder(repository);
//const creator = buildProformaCreator(numberService, repository);
const snapshotBuilders = buildProformaSnapshotBuilders();
const documentGeneratorPipeline = buildproformaDocumentService(params);

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../common/dto/index.ts";
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto/index.ts";
export class ChangeStatusProformaController extends ExpressController {
public constructor(private readonly useCase: ChangeStatusProformaUseCase) {

View File

@ -5,9 +5,9 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { CreateProformaRequestDTO } from "../../../../common/dto/index.ts";
import type { CreateProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { CreateProformaRequestDTO } from "../../../../../common/dto/index.ts";
import type { CreateProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class CreateProformaController extends ExpressController {
public constructor(private readonly useCase: CreateProformaUseCase) {

View File

@ -5,8 +5,8 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeleteProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { DeleteProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class DeleteProformaController extends ExpressController {
public constructor(private readonly useCase: DeleteProformaUseCase) {

View File

@ -5,8 +5,8 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { GetProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { GetProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class GetProformaController extends ExpressController {
public constructor(private readonly useCase: GetProformaUseCase) {

View File

@ -0,0 +1,8 @@
//export * from "./change-status-proforma.controller";
//export * from "./create-proforma.controller";
//export * from "./delete-proforma.controller";
export * from "./get-proforma.controller";
//export * from "./issue-proforma.controller";
export * from "./list-proformas.controller";
export * from "./report-proforma.controller";
//export * from "./update-proforma.controller";

View File

@ -5,8 +5,8 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { IssueProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { IssueProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class IssueProformaController extends ExpressController {
public constructor(private readonly useCase: IssueProformaUseCase) {

View File

@ -6,8 +6,8 @@ import {
} from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListProformasUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { ListProformasUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class ListProformasController extends ExpressController {
public constructor(private readonly useCase: ListProformasUseCase) {

View File

@ -5,8 +5,8 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { ReportProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { ReportProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class ReportProformaController extends ExpressController {
public constructor(private readonly useCase: ReportProformaUseCase) {

View File

@ -5,9 +5,9 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto/index.ts";
import type { UpdateProformaUseCase } from "../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../express/proformas/proformas-api-error-mapper.ts";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/index.ts";
import type { UpdateProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class UpdateProformaController extends ExpressController {
public constructor(private readonly useCase: UpdateProformaUseCase) {

View File

@ -1,8 +1 @@
//export * from "./change-status-proforma.controller";
//export * from "./create-proforma.controller";
//export * from "./delete-proforma.controller";
export * from "./get-proforma.controller";
//export * from "./issue-proforma.controller";
export * from "./list-proformas.controller";
export * from "./report-proforma.controller";
//export * from "./update-proforma.controller";
export * from "./controllers";

View File

@ -1,4 +1,4 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
@ -15,7 +15,6 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IProformaDomainMapper } from "../../../../../../application";
import {
DiscountPercentage,
InvoiceNumber,
InvoicePaymentMethod,
InvoiceSerie,
@ -191,14 +190,14 @@ export class SequelizeProformaDomainMapper
// 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(raw, {
errors,
attributes,
parent: attributes,
...params,
});
// 3) Items (colección)
const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
errors,
attributes,
parent: attributes,
...params,
});
@ -281,7 +280,7 @@ export class SequelizeProformaDomainMapper
}
// 2) Taxes
const taxesResult = this._taxesMapper.mapToPersistenceArray(source.getTaxes(), {
const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes(), {
errors,
parent: source,
...params,
@ -311,7 +310,7 @@ export class SequelizeProformaDomainMapper
const items = itemsResult.data;
const taxes = taxesResult.data;
const allAmounts = source.calculateAllAmounts(); // Da los totales ya calculados
const allAmounts = source.totals(); // Da los totales ya calculados
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
// Identificación

View File

@ -1,5 +1,10 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api";
import {
DiscountPercentage,
type MapperParamsType,
SequelizeDomainMapper,
Tax,
} from "@erp/core/api";
import {
UniqueID,
ValidationErrorCollection,
@ -13,12 +18,12 @@ import { Result } from "@repo/rdx-utils";
import {
ItemAmount,
ItemDescription,
ItemDiscountPercentage,
ItemQuantity,
ItemTaxGroup,
type Proforma,
ProformaItem,
type ProformaItemProps,
ProformaItemTaxes,
type ProformaItemTaxesProps,
type ProformaProps,
} from "../../../../../../domain";
import type {
@ -47,75 +52,63 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
}
private mapAttributesToDomain(
source: CustomerInvoiceItemModel,
raw: CustomerInvoiceItemModel,
params?: MapperParamsType
): Partial<ProformaItemProps> & { itemId?: UniqueID } {
const { errors, index, attributes } = params as {
): Partial<ProformaItemProps & ProformaItemTaxesProps> & { itemId?: UniqueID } {
const { errors, index, parent } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<ProformaProps>;
parent: Partial<ProformaProps>;
};
const itemId = extractOrPushError(
UniqueID.create(source.item_id),
UniqueID.create(raw.item_id),
`items[${index}].item_id`,
errors
);
const description = extractOrPushError(
maybeFromNullableResult(source.description, (v) => ItemDescription.create(v)),
maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)),
`items[${index}].description`,
errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(source.quantity_value, (v) => ItemQuantity.create({ value: v })),
maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })),
`items[${index}].quantity`,
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(source.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: attributes.currencyCode?.code })
maybeFromNullableResult(raw.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: parent.currencyCode?.code })
),
`items[${index}].unit_amount`,
errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(source.discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v })
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
DiscountPercentage.create({ value: v })
),
`items[${index}].discount_percentage`,
errors
);
const globalDiscountPercentage = extractOrPushError(
maybeFromNullableResult(source.global_discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v })
),
`items[${index}].discount_percentage`,
`items[${index}].item_discount_percentage`,
errors
);
const iva = extractOrPushError(
maybeFromNullableResult(source.iva_code, (code) =>
Tax.createFromCode(code, this._taxCatalog)
),
maybeFromNullableResult(raw.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)),
`items[${index}].iva_code`,
errors
);
const rec = extractOrPushError(
maybeFromNullableResult(source.rec_code, (code) =>
Tax.createFromCode(code, this._taxCatalog)
),
maybeFromNullableResult(raw.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)),
`items[${index}].rec_code`,
errors
);
const retention = extractOrPushError(
maybeFromNullableResult(source.retention_code, (code) =>
maybeFromNullableResult(raw.retention_code, (code) =>
Tax.createFromCode(code, this._taxCatalog)
),
`items[${index}].retention_code`,
@ -125,34 +118,32 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
return {
itemId,
languageCode: attributes.languageCode,
currencyCode: attributes.currencyCode,
languageCode: parent.languageCode,
currencyCode: parent.currencyCode,
description,
quantity,
unitAmount,
itemDiscountPercentage: discountPercentage,
globalDiscountPercentage,
itemDiscountPercentage,
globalDiscountPercentage: parent.globalDiscountPercentage,
taxes: ItemTaxGroup.create({
iva: iva!,
rec: rec!,
retention: retention!,
}).data,
iva,
rec,
retention,
};
}
public mapToDomain(
source: CustomerInvoiceItemModel,
raw: CustomerInvoiceItemModel,
params?: MapperParamsType
): Result<ProformaItem, Error> {
const { errors, index } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<ProformaProps>;
parent: Partial<ProformaProps>;
};
// 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDomain(source, params);
const attributes = this.mapAttributesToDomain(raw, params);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
@ -161,6 +152,13 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
);
}
// 2) Construcción del elemento de taxes
const taxesResult = ProformaItemTaxes.create({
iva: attributes.iva!,
rec: attributes.rec!,
retention: attributes.retention!,
});
// 2) Construcción del elemento de dominio
const createResult = ProformaItem.create(
{
@ -171,7 +169,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
unitAmount: attributes.unitAmount!,
itemDiscountPercentage: attributes.itemDiscountPercentage!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
taxes: attributes.taxes!,
taxes: taxesResult.data,
},
attributes.itemId
);
@ -197,8 +195,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
errors: ValidationErrorDetail[];
};
const allAmounts = source.calculateAllAmounts();
const taxesAmounts = source.taxes.calculateAmounts(allAmounts.taxableAmount);
const allAmounts = source.totals();
return Result.ok({
item_id: source.id.toPrimitive(),
@ -221,26 +218,20 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
subtotal_amount_scale: allAmounts.subtotalAmount.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,
DiscountPercentage.DEFAULT_SCALE,
discount_amount_value: allAmounts.itemDiscountAmount.value,
discount_amount_scale: allAmounts.itemDiscountAmount.scale,
item_discount_amount_value: allAmounts.itemDiscountAmount.value,
item_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
//
global_discount_percentage_value: maybeToNullable(
source.globalDiscountPercentage,
(v) => v.toPrimitive().value
),
global_discount_percentage_scale:
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE,
global_discount_percentage_value: source.globalDiscountPercentage.value,
global_discount_percentage_scale: source.globalDiscountPercentage.scale,
global_discount_amount_value: allAmounts.globalDiscountAmount.value,
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,
@ -257,19 +248,21 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
iva_code: maybeToNullable(source.taxes.iva, (v) => v.code),
iva_percentage_value: maybeToNullable(source.taxes.iva, (v) => v.percentage.value),
iva_percentage_scale: maybeToNullable(source.taxes.iva, (v) => v.percentage.scale) ?? 2,
iva_percentage_scale:
maybeToNullable(source.taxes.iva, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
iva_amount_value: taxesAmounts.ivaAmount.value,
iva_amount_scale: taxesAmounts.ivaAmount.scale,
iva_amount_value: allAmounts.ivaAmount.value,
iva_amount_scale: allAmounts.ivaAmount.scale,
// REC
rec_code: maybeToNullable(source.taxes.rec, (v) => v.code),
rec_percentage_value: maybeToNullable(source.taxes.rec, (v) => v.percentage.value),
rec_percentage_scale: maybeToNullable(source.taxes.rec, (v) => v.percentage.scale) ?? 2,
rec_percentage_scale:
maybeToNullable(source.taxes.rec, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
rec_amount_value: taxesAmounts.recAmount.value,
rec_amount_scale: taxesAmounts.recAmount.scale,
rec_amount_value: allAmounts.recAmount.value,
rec_amount_scale: allAmounts.recAmount.scale,
// RET
retention_code: maybeToNullable(source.taxes.retention, (v) => v.code),
@ -279,10 +272,10 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
(v) => v.percentage.value
),
retention_percentage_scale:
maybeToNullable(source.taxes.retention, (v) => v.percentage.scale) ?? 2,
maybeToNullable(source.taxes.retention, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
retention_amount_value: taxesAmounts.retentionAmount.value,
retention_amount_scale: taxesAmounts.retentionAmount.scale,
retention_amount_value: allAmounts.retentionAmount.value,
retention_amount_scale: allAmounts.retentionAmount.scale,
//
taxes_amount_value: allAmounts.taxesAmount.value,

View File

@ -26,20 +26,18 @@ export class SequelizeProformaRecipientDomainMapper {
* En proforma -> datos de "current_customer"
*/
const { errors, attributes } = params as {
const { errors, parent } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<ProformaProps>;
parent: Partial<ProformaProps>;
};
const { isProforma } = attributes;
if (isProforma && !source.current_customer) {
/* if (!source.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (SequelizeProformaRecipientDomainMapper)",
});
}
*/
const _name = source.current_customer.name;
const _tin = source.current_customer.tin;
const _street = source.current_customer.street;

View File

@ -1,9 +1,9 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api";
import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { InvoiceTaxGroup, Proforma } from "../../../../../../domain";
import type { IProformaTaxTotals, Proforma } from "../../../../../../domain";
import type {
CustomerInvoiceTaxCreationAttributes,
CustomerInvoiceTaxModel,
@ -24,7 +24,7 @@ import type {
export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxModel,
CustomerInvoiceTaxCreationAttributes,
InvoiceTaxGroup
IProformaTaxTotals
> {
private taxCatalog!: JsonTaxCatalogProvider;
@ -44,12 +44,12 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
public mapToDomain(
source: CustomerInvoiceTaxModel,
params?: MapperParamsType
): Result<InvoiceTaxGroup, Error> {
): Result<IProformaTaxTotals, Error> {
throw new Error("Se calcula a partir de las líneas de detalle");
}
public mapToPersistence(
source: InvoiceTaxGroup,
source: IProformaTaxTotals,
params?: MapperParamsType
): Result<CustomerInvoiceTaxCreationAttributes, Error> {
const { errors, parent } = params as {
@ -58,10 +58,6 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
};
try {
const { ivaAmount, recAmount, retentionAmount, totalAmount } = source.calculateAmounts();
const totalTaxes = totalAmount;
const dto: CustomerInvoiceTaxCreationAttributes = {
tax_id: UniqueID.generateNewID().toPrimitive(),
invoice_id: parent.id.toPrimitive(),
@ -71,36 +67,39 @@ export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper<
taxable_amount_scale: source.taxableAmount.scale,
// IVA
iva_code: source.iva.code,
iva_code: maybeToNullable(source.ivaCode, (v) => v),
iva_percentage_value: source.iva.value,
iva_percentage_scale: source.iva.scale,
iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.value),
iva_percentage_scale:
maybeToNullable(source.ivaPercentage, (v) => v.scale) ?? TaxPercentage.DEFAULT_SCALE,
iva_amount_value: ivaAmount.value,
iva_amount_scale: ivaAmount.scale,
iva_amount_value: source.ivaAmount.value,
iva_amount_scale: source.ivaAmount.scale,
// REC
rec_code: maybeToNullable(source.rec, (v) => v.code),
rec_code: maybeToNullable(source.recCode, (v) => v),
rec_percentage_value: maybeToNullable(source.rec, (v) => v.percentage.value),
rec_percentage_scale: maybeToNullable(source.rec, (v) => v.percentage.scale) ?? 2,
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.value),
rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.scale) ?? TaxPercentage.DEFAULT_SCALE,
rec_amount_value: recAmount.value,
rec_amount_scale: recAmount.scale,
rec_amount_value: source.recAmount.value,
rec_amount_scale: source.recAmount.scale,
// RET
retention_code: maybeToNullable(source.retention, (v) => v.code),
retention_code: maybeToNullable(source.retentionCode, (v) => v),
retention_percentage_value: maybeToNullable(source.retention, (v) => v.percentage.value),
retention_percentage_value: maybeToNullable(source.retentionPercentage, (v) => v.value),
retention_percentage_scale:
maybeToNullable(source.retention, (v) => v.percentage.scale) ?? 2,
maybeToNullable(source.retentionPercentage, (v) => v.scale) ??
TaxPercentage.DEFAULT_SCALE,
retention_amount_value: retentionAmount.value,
retention_amount_scale: retentionAmount.scale,
retention_amount_value: source.retentionAmount.value,
retention_amount_scale: source.retentionAmount.scale,
// TOTAL
taxes_amount_value: totalTaxes.value,
taxes_amount_scale: totalTaxes.scale,
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
};
return Result.ok(dto);