This commit is contained in:
David Arranz 2026-02-19 15:51:30 +01:00
parent 302f3293c6
commit acd61cf6eb
26 changed files with 444 additions and 207 deletions

View File

@ -5,6 +5,7 @@ import {
IssuedInvoiceFullSnapshotBuilder,
IssuedInvoiceItemsFullSnapshotBuilder,
IssuedInvoiceRecipientFullSnapshotBuilder,
IssuedInvoiceTaxesFullSnapshotBuilder,
IssuedInvoiceVerifactuFullSnapshotBuilder,
} from "../snapshot-builders/full";
import {
@ -16,6 +17,8 @@ import {
export function buildIssuedInvoiceSnapshotBuilders() {
const itemsBuilder = new IssuedInvoiceItemsFullSnapshotBuilder();
const taxesBuilder = new IssuedInvoiceTaxesFullSnapshotBuilder();
const recipientBuilder = new IssuedInvoiceRecipientFullSnapshotBuilder();
const verifactuBuilder = new IssuedInvoiceVerifactuFullSnapshotBuilder();
@ -23,7 +26,8 @@ export function buildIssuedInvoiceSnapshotBuilders() {
const fullSnapshotBuilder = new IssuedInvoiceFullSnapshotBuilder(
itemsBuilder,
recipientBuilder,
verifactuBuilder
verifactuBuilder,
taxesBuilder
);
const listSnapshotBuilder = new IssuedInvoiceListItemSnapshotBuilder();

View File

@ -2,7 +2,9 @@ export * from "./issued-invoice-full-snapshot.interface";
export * from "./issued-invoice-full-snapshot-builder";
export * from "./issued-invoice-item-full-snapshot.interface";
export * from "./issued-invoice-items-full-snapshot-builder";
export * from "./issued-invoice-recipient-full-snapshot.interfce";
export * from "./issued-invoice-recipient-full-snapshot.interface";
export * from "./issued-invoice-recipient-full-snapshot-builder";
export * from "./issued-invoice-tax-full-snapshot-interface";
export * from "./issued-invoice-taxes-full-snapshot-builder";
export * from "./issued-invoice-verifactu-full-snapshot.interface";
export * from "./issued-invoice-verifactu-full-snapshot-builder";

View File

@ -1,11 +1,12 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import { InvoiceAmount, type IssuedInvoice } from "../../../../domain";
import type { IssuedInvoice } from "../../../../domain";
import type { IIssuedInvoiceFullSnapshot } from "./issued-invoice-full-snapshot.interface";
import type { IIssuedInvoiceItemsFullSnapshotBuilder } from "./issued-invoice-items-full-snapshot-builder";
import type { IIssuedInvoiceRecipientFullSnapshotBuilder } from "./issued-invoice-recipient-full-snapshot-builder";
import type { IIssuedInvoiceTaxesFullSnapshotBuilder } from "./issued-invoice-taxes-full-snapshot-builder";
import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder";
export interface IIssuedInvoiceFullSnapshotBuilder
@ -15,13 +16,15 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
constructor(
private readonly itemsBuilder: IIssuedInvoiceItemsFullSnapshotBuilder,
private readonly recipientBuilder: IIssuedInvoiceRecipientFullSnapshotBuilder,
private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder
private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder,
private readonly taxesBuilder: IIssuedInvoiceTaxesFullSnapshotBuilder
) {}
toOutput(invoice: IssuedInvoice): IIssuedInvoiceFullSnapshot {
const items = this.itemsBuilder.toOutput(invoice.items);
const recipient = this.recipientBuilder.toOutput(invoice);
const verifactu = this.verifactuBuilder.toOutput(invoice);
const taxes = this.taxesBuilder.toOutput(invoice.taxes);
const payment = invoice.paymentMethod.match(
(payment) => {
@ -34,57 +37,11 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
() => 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.taxes().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(),
};
});
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
is_proforma: invoice.isProforma ? "true" : "false",
is_proforma: "false",
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: maybeToEmptyString(invoice.series, (value) => value.toString()),
@ -104,25 +61,26 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
payment_method: payment,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
subtotal_amount: invoice.subtotalAmount.toObjectString(),
items_discount_amount: invoice.itemsDiscountAmount.toObjectString(),
discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
global_discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
global_discount_amount: invoice.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
total_discount_amount: invoice.totalDiscountAmount.toObjectString(),
iva_amount: totalIvaAmount.toObjectString(),
rec_amount: totalRecAmount.toObjectString(),
retention_amount: totalRetentionAmount.toObjectString(),
taxable_amount: invoice.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
iva_amount: invoice.ivaAmount.toObjectString(),
rec_amount: invoice.recAmount.toObjectString(),
retention_amount: invoice.retentionAmount.toObjectString(),
taxes: invoiceTaxes,
taxes_amount: invoice.taxesAmount.toObjectString(),
total_amount: invoice.totalAmount.toObjectString(),
taxes,
verifactu,
items,
metadata: {

View File

@ -1,5 +1,6 @@
import type { IIssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot.interface";
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce";
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface";
import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface";
import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface";
export interface IIssuedInvoiceFullSnapshot {
@ -30,10 +31,11 @@ export interface IIssuedInvoiceFullSnapshot {
};
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 };
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 };
@ -44,23 +46,7 @@ export interface IIssuedInvoiceFullSnapshot {
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: IIssuedInvoiceTaxFullSnapshot[];
verifactu: IIssuedInvoiceVerifactuFullSnapshot;
items: IIssuedInvoiceItemFullSnapshot[];

View File

@ -1,5 +1,10 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import {
maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString,
maybeToEmptyQuantityObjectString,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import type { IssuedInvoiceItem, IssuedInvoiceItems } from "../../../../domain";
@ -16,75 +21,38 @@ export class IssuedInvoiceItemsFullSnapshotBuilder
id: invoiceItem.id.toPrimitive(),
is_valued: String(invoiceItem.isValued),
position: String(index),
description: maybeToEmptyString(invoiceItem.description, (value) => value.toPrimitive()),
quantity: invoiceItem.quantity.match(
(quantity) => quantity.toObjectString(),
() => ({ value: "", scale: "" })
description: maybeToEmptyString(invoiceItem.description, (value) => value.toString()),
quantity: maybeToEmptyQuantityObjectString(invoiceItem.quantity),
unit_amount: maybeToEmptyMoneyObjectString(invoiceItem.unitAmount),
subtotal_amount: maybeToEmptyMoneyObjectString(invoiceItem.subtotalAmount),
discount_percentage: maybeToEmptyPercentageObjectString(invoiceItem.itemDiscountPercentage),
discount_amount: maybeToEmptyMoneyObjectString(invoiceItem.itemDiscountAmount),
global_discount_percentage: maybeToEmptyPercentageObjectString(
invoiceItem.globalDiscountPercentage
),
global_discount_amount: maybeToEmptyMoneyObjectString(invoiceItem.globalDiscountAmount),
unit_amount: invoiceItem.unitAmount.match(
(unitAmount) => unitAmount.toObjectString(),
() => ({ value: "", scale: "", currency_code: "" })
),
taxable_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxableAmount),
subtotal_amount: invoiceItem.subtotalAmount.toObjectString(),
iva_code: maybeToEmptyString(invoiceItem.ivaCode),
iva_percentage: maybeToEmptyPercentageObjectString(invoiceItem.ivaPercentage),
iva_amount: maybeToEmptyMoneyObjectString(invoiceItem.ivaAmount),
discount_percentage: invoiceItem.itemDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_code: maybeToEmptyString(invoiceItem.recCode),
rec_percentage: maybeToEmptyPercentageObjectString(invoiceItem.recPercentage),
rec_amount: maybeToEmptyMoneyObjectString(invoiceItem.recAmount),
discount_amount: invoiceItem.itemDiscountAmount.toObjectString(),
retention_code: maybeToEmptyString(invoiceItem.retentionCode),
retention_percentage: maybeToEmptyPercentageObjectString(invoiceItem.retentionPercentage),
retention_amount: maybeToEmptyMoneyObjectString(invoiceItem.retentionAmount),
global_discount_percentage: invoiceItem.globalDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
global_discount_amount: invoiceItem.globalDiscountAmount.toObjectString(),
taxable_amount: invoiceItem.taxableAmount.toObjectString(),
iva_code: invoiceItem.taxes.iva.match(
(iva) => iva.code,
() => ""
),
iva_percentage: invoiceItem.taxes.iva.match(
(iva) => iva.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
iva_amount: invoiceItem.ivaAmount.toObjectString(),
rec_code: invoiceItem.taxes.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: invoiceItem.taxes.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: invoiceItem.recAmount.toObjectString(),
retention_code: invoiceItem.taxes.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: invoiceItem.taxes.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: invoiceItem.retentionAmount.toObjectString(),
taxes_amount: invoiceItem.taxesAmount.toObjectString(),
total_amount: invoiceItem.totalAmount.toObjectString(),
taxes_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxesAmount),
total_amount: maybeToEmptyMoneyObjectString(invoiceItem.totalAmount),
};
}

View File

@ -3,7 +3,7 @@ import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd";
import type { InvoiceRecipient, IssuedInvoice } from "../../../../domain";
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce";
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface";
export interface IIssuedInvoiceRecipientFullSnapshotBuilder
extends ISnapshotBuilder<IssuedInvoice, IIssuedInvoiceRecipientFullSnapshot> {}

View File

@ -0,0 +1,17 @@
export interface IIssuedInvoiceTaxFullSnapshot {
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 };
}

View File

@ -0,0 +1,41 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import {
maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import type { IssuedInvoiceTax, IssuedInvoiceTaxes } from "../../../../domain";
import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface";
export interface IIssuedInvoiceTaxesFullSnapshotBuilder
extends ISnapshotBuilder<IssuedInvoiceTaxes, IIssuedInvoiceTaxFullSnapshot[]> {}
export class IssuedInvoiceTaxesFullSnapshotBuilder
implements IIssuedInvoiceTaxesFullSnapshotBuilder
{
private mapItem(invoiceTax: IssuedInvoiceTax, index: number): IIssuedInvoiceTaxFullSnapshot {
return {
taxable_amount: invoiceTax.taxableAmount.toObjectString(),
iva_code: invoiceTax.ivaCode.toString(),
iva_percentage: invoiceTax.ivaPercentage.toObjectString(),
iva_amount: invoiceTax.ivaAmount.toObjectString(),
rec_code: maybeToEmptyString(invoiceTax.recCode),
rec_percentage: maybeToEmptyPercentageObjectString(invoiceTax.recPercentage),
rec_amount: maybeToEmptyMoneyObjectString(invoiceTax.recAmount),
retention_code: maybeToEmptyString(invoiceTax.retentionCode),
retention_percentage: maybeToEmptyPercentageObjectString(invoiceTax.retentionPercentage),
retention_amount: maybeToEmptyMoneyObjectString(invoiceTax.retentionAmount),
taxes_amount: invoiceTax.taxesAmount.toObjectString(),
};
}
toOutput(invoiceTaxes: IssuedInvoiceTaxes): IIssuedInvoiceTaxFullSnapshot[] {
return invoiceTaxes.map((item, index) => this.mapItem(item, index));
}
}

View File

@ -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";

View File

@ -17,8 +17,6 @@ import { Maybe, Result } from "@repo/rdx-utils";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
import {
CustomerInvoiceItems,
type IProformaProps,
InvoiceNumber,
InvoicePaymentMethod,
type InvoiceRecipient,

View File

@ -55,6 +55,11 @@ export type IssuedInvoiceProps = {
totalDiscountAmount: InvoiceAmount;
taxableAmount: InvoiceAmount;
ivaAmount: InvoiceAmount;
recAmount: InvoiceAmount;
retentionAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
totalAmount: InvoiceAmount;
@ -188,6 +193,18 @@ export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> {
return this.props.taxableAmount;
}
public get ivaAmount(): InvoiceAmount {
return this.props.ivaAmount;
}
public get recAmount(): InvoiceAmount {
return this.props.recAmount;
}
public get retentionAmount(): InvoiceAmount {
return this.props.retentionAmount;
}
public get taxesAmount(): InvoiceAmount {
return this.props.taxesAmount;
}

View File

@ -1,4 +1,3 @@
export * from "./issued-invoice-items";
export * from "./issued-invoice-tax.entity";
export * from "./issued-invoice-taxes.collection";
export * from "./issued-invoice-taxes";
export * from "./verifactu-record.entity";

View File

@ -1,7 +1,7 @@
import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount, ItemDiscountPercentage } from "../../../common";
import { ItemDiscountPercentage } from "../../../common";
import type { IssuedInvoiceItem } from "./issued-invoice-item.entity";
@ -57,10 +57,10 @@ export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> {
return super.add(item);
}
public getTotalAmount(): InvoiceAmount {
/*public getTotalAmount(): InvoiceAmount {
return this.getAll().reduce(
(acc, item) => acc.add(item.getProps().totalAmount),
InvoiceAmount.zero(this.getAll()[0]?.getProps().totalAmount.currencyCode ?? "EUR")
);
}
}*/
}

View File

@ -1,13 +0,0 @@
import { Collection } from "@repo/rdx-utils";
import type { IssuedInvoiceTax } from "./issued-invoice-tax.entity";
export class IssuedInvoiceTaxes extends Collection<IssuedInvoiceTax> {
constructor(items: IssuedInvoiceTax[] = []) {
super(items);
}
public static create(items: IssuedInvoiceTax[] = []): IssuedInvoiceTaxes {
return new IssuedInvoiceTaxes(items);
}
}

View File

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

View File

@ -1,7 +1,7 @@
import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../common";
import type { InvoiceAmount } from "../../../common";
export type IssuedInvoiceTaxProps = {
taxableAmount: InvoiceAmount;

View File

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

View File

@ -88,6 +88,18 @@ export class CustomerInvoiceModel extends Model<
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// IVA amount
declare iva_amount_value: number;
declare iva_amount_scale: number;
// Recargo de equivalencia amount
declare rec_amount_value: number;
declare rec_amount_scale: number;
// Retention amount
declare retention_amount_value: number;
declare retention_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
@ -350,6 +362,40 @@ export default (database: Sequelize) => {
defaultValue: 2,
},
iva_amount_value: {
type: DataTypes.BIGINT,
allowNull: true,
defaultValue: null,
},
iva_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
rec_amount_value: {
type: DataTypes.BIGINT,
allowNull: true,
defaultValue: null,
},
rec_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
retention_amount_value: {
type: DataTypes.BIGINT,
allowNull: true,
defaultValue: null,
},
retention_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false,

View File

@ -216,6 +216,33 @@ export class SequelizeIssuedInvoiceDomainMapper
errors
);
const ivaAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.iva_amount_value,
currency_code: currencyCode?.code,
}),
"iva_amount_value",
errors
);
const recAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.rec_amount_value,
currency_code: currencyCode?.code,
}),
"rec_amount_value",
errors
);
const retentionAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.retention_amount_value,
currency_code: currencyCode?.code,
}),
"retention_amount_value",
errors
);
const taxesAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxes_amount_value,
@ -258,6 +285,9 @@ export class SequelizeIssuedInvoiceDomainMapper
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
ivaAmount,
recAmount,
retentionAmount,
taxesAmount,
totalAmount,
};
@ -318,18 +348,19 @@ export class SequelizeIssuedInvoiceDomainMapper
// 6) Construcción del agregado (Dominio)
const verifactu = verifactuResult.data;
const items = IssuedInvoiceItems.create({
items: itemsResults.data.getAll(),
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
items: itemsResults.data.getAll(),
});
const taxes = IssuedInvoiceTaxes.create({
taxes: taxesResults.data.getAll(),
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
taxes: taxesResults.data.getAll(),
});
const invoiceProps: IssuedInvoiceProps = {
@ -360,14 +391,18 @@ export class SequelizeIssuedInvoiceDomainMapper
totalDiscountAmount: attributes.totalDiscountAmount!,
taxableAmount: attributes.taxableAmount!,
ivaAmount: attributes.ivaAmount!,
recAmount: attributes.recAmount!,
retentionAmount: attributes.retentionAmount!,
taxesAmount: attributes.taxesAmount!,
totalAmount: attributes.totalAmount!,
paymentMethod: attributes.paymentMethod!,
items,
taxes: taxesResults.data,
verifactu: verifactuResult.data,
taxes,
verifactu,
};
const createResult = IssuedInvoice.create(invoiceProps, attributes.invoiceId);
@ -479,11 +514,17 @@ export class SequelizeIssuedInvoiceDomainMapper
subtotal_amount_value: source.subtotalAmount.value,
subtotal_amount_scale: source.subtotalAmount.scale,
discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
items_discount_amount_value: source.itemsDiscountAmount.value,
items_discount_amount_scale: source.itemsDiscountAmount.scale,
discount_amount_value: source.globalDiscountAmount.value,
discount_amount_scale: source.globalDiscountAmount.scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
global_discount_amount_value: source.globalDiscountAmount.value,
global_discount_amount_scale: source.globalDiscountAmount.scale,
total_discount_amount_value: source.totalDiscountAmount.value,
total_discount_amount_scale: source.totalDiscountAmount.scale,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,

View File

@ -339,10 +339,6 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.subtotalAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
// Te has quedado aquí --- IGNORE ---
// !!!!!!!!!!!!!!!!!!!
//
discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage,
(v) => v.toPrimitive().value
@ -351,29 +347,46 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE,
discount_amount_value: source.itemDiscountAmount.value,
discount_amount_scale: source.itemDiscountAmount.scale,
discount_amount_value: maybeToNullable(
source.itemDiscountAmount,
(v) => v.toPrimitive().value
),
discount_amount_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_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_amount_value: source.globalDiscountAmount.value,
global_discount_amount_scale: source.globalDiscountAmount.scale,
global_discount_amount_value: maybeToNullable(
source.globalDiscountAmount,
(v) => v.toPrimitive().value
),
global_discount_amount_scale:
maybeToNullable(source.globalDiscountAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
total_discount_amount_value: maybeToNullable(
source.totalDiscountAmount,
(v) => v.toPrimitive().value
),
total_discount_amount_scale:
maybeToNullable(source.totalDiscountAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
// Te has quedado aquí --- IGNORE ---
// !!!!!!!!!!!!!!!!!!!
//
total_discount_amount_value: source.totalDiscountAmount.value,
total_discount_amount_scale: source.totalDiscountAmount.scale,
//
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
taxable_amount_value: maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().value),
taxable_amount_scale:
maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
// IVA
iva_code: maybeToNullableString(source.ivaCode),
@ -410,12 +423,16 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4,
//
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
taxes_amount_value: maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().value),
taxes_amount_scale:
maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
//
total_amount_value: source.totalAmount.value,
total_amount_scale: source.totalAmount.scale,
total_amount_value: maybeToNullable(source.totalAmount, (v) => v.toPrimitive().value),
total_amount_scale:
maybeToNullable(source.totalAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
});
}
}

View File

@ -62,8 +62,9 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema,
iva_amount: MoneySchema,
rec_amount: MoneySchema,

View File

@ -1,8 +1,9 @@
// application/shared/normalizers.ts
// Normalizadores y adaptadores DTO -> Maybe/VO
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { MoneyValue, Percentage, Quantity } from "../value-objects";
/** any | null | undefined -> Maybe<T> usando validación */
export function maybeFromNullableResult<T>(
input: any,
@ -13,6 +14,115 @@ export function maybeFromNullableResult<T>(
return value.isSuccess ? Result.ok(Maybe.some(value.data)) : Result.fail(value.error);
}
/**
* Serializa un Maybe aplicando una política de objeto vacío.
*
* @internal
*/
function maybeToEmptyObjectString<T, R>(
maybe: Maybe<T>,
emptyObject: R,
defaultSerializer: (value: T) => R,
map?: (value: T) => R
): R {
if (!maybe || maybe.isNone()) {
return emptyObject;
}
const value = maybe.unwrap();
return map ? map(value) : defaultSerializer(value);
}
/**
* Serializa un `Maybe<MoneyValue>` a un objeto de transporte normalizado.
*
* - Si el `Maybe` es `None`, devuelve un objeto vacío:
* `{ value: "", scale: "", currency_code: "" }`
* - Si es `Some`, aplica la función `map` si está declarada o
* aplica el método `toObjectString()` del `MoneyValue`.
*
* Motivación:
* - Evita devolver `null` en la capa de transporte.
* - Garantiza una estructura estable para consumidores (API / frontend).
* - Centraliza la política de normalización de importes opcionales.
*
* @typeParam T - Tipo interno del MoneyValue/Amount en dominio.
* @param maybe - Instancia `Maybe` que envuelve el Amount de dominio.
* @param map - Función que transforma el Amount de dominio al objeto de transporte.
* @returns Objeto normalizado de importe listo para transporte.
*/
export function maybeToEmptyMoneyObjectString(
maybe: Maybe<MoneyValue>,
map?: (value: MoneyValue) => { value: string; scale: string; currency_code: string }
): { value: string; scale: string; currency_code: string } {
return maybeToEmptyObjectString(
maybe,
MoneyValue.EMPTY_MONEY_OBJECT,
(value) => value.toObjectString(),
map
);
}
/**
* Serializa un `Maybe<Percentage>` a un objeto de transporte normalizado.
*
* - Si el `Maybe` es `None`, devuelve un objeto vacío:
* `{ value: "", scale: "" }`
* - Si es `Some`, aplica la función `map` si está declarada o
* aplica el método `toObjectString()` del `Percentage`.
*
* Motivación:
* - Evita devolver `null` en la capa de transporte.
* - Garantiza una estructura estable para consumidores (API / frontend).
* - Centraliza la política de normalización de porcentajes opcionales.
*
* @typeParam T - Tipo interno del Percentage en dominio.
* @param maybe - Instancia `Maybe` que envuelve el Percentage de dominio.
* @param map - Función que transforma el Percentage de dominio al objeto de transporte.
* @returns Objeto normalizado de porcentaje listo para transporte.
*/
export function maybeToEmptyPercentageObjectString(
maybe: Maybe<Percentage>,
map?: (value: Percentage) => { value: string; scale: string }
): { value: string; scale: string } {
return maybeToEmptyObjectString(
maybe,
Percentage.EMPTY_PERCENTAGE_OBJECT,
(value) => value.toObjectString(),
map
);
}
/**
* Serializa un `Maybe<Quantity>` a un objeto de transporte normalizado.
*
* - Si el `Maybe` es `None`, devuelve un objeto vacío:
* `{ value: "", scale: "" }`
* - Si es `Some`, aplica la función `map` si está declarada o
* aplica el método `toObjectString()` del `Quantity`.
*
* Motivación:
* - Evita devolver `null` en la capa de transporte.
* - Garantiza una estructura estable para consumidores (API / frontend).
* - Centraliza la política de normalización de cantidades opcionales.
*
* @typeParam T - Tipo interno del Quantity en dominio.
* @param maybe - Instancia `Maybe` que envuelve el Quantity de dominio.
* @param map - Función que transforma el Quantity de dominio al objeto de transporte.
* @returns Objeto normalizado de cantidad listo para transporte.
*/
export function maybeToEmptyQuantityObjectString(
maybe: Maybe<Quantity>,
map?: (value: Quantity) => { value: string; scale: string }
): { value: string; scale: string } {
return maybeToEmptyObjectString(
maybe,
Quantity.EMPTY_QUANTITY_OBJECT,
(value) => value.toObjectString(),
map
);
}
/** string | null | undefined -> Maybe<string> (trim, vacío => None) */
export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<string> {
if (isNullishOrEmpty(input)) return Maybe.none<string>();

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils";
import DineroFactory, { Currency, Dinero } from "dinero.js";
import { Percentage } from "./percentage";
import { Quantity } from "./quantity";
import DineroFactory, { type Currency, type Dinero } from "dinero.js";
import type { Percentage } from "./percentage";
import type { Quantity } from "./quantity";
import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2;
@ -45,6 +46,8 @@ export interface IMoneyValue {
hasSameCurrency(comparator: MoneyValue): boolean;
hasSameAmount(comparator: MoneyValue): boolean;
format(locale: string): string;
toObjectString(): { value: string; scale: string; currency_code: string };
}
export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyValue {
@ -52,6 +55,7 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
static DEFAULT_SCALE = DEFAULT_SCALE;
static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE;
static EMPTY_MONEY_OBJECT = { value: "", scale: "", currency_code: "" };
static create({ value, currency_code, scale }: MoneyValueProps) {
const props = {
@ -95,13 +99,13 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
}
/** Reconstruye el VO desde la cadena persistida */
static fromPersistence(value: string): MoneyValue {
/*static fromPersistence(value: string): MoneyValue {
const [currencyCode, amountStr, scaleStr] = value.split(":");
const amount = Number.parseInt(amountStr, 10);
const scale = Number.parseInt(scaleStr, 10);
const currency = currencyCode;
return new MoneyValue({ value: amount, scale, currency_code: currency });
}
}*/
toPrimitive() {
return {
@ -111,6 +115,14 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
};
}
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
currency_code: this.currencyCode,
};
}
convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
const _newDinero = this.dinero.convertPrecision(newScale, roundingMode);
return new MoneyValue({

View File

@ -26,6 +26,8 @@ export class Percentage extends ValueObject<PercentageProps> {
static MIN_SCALE = DEFAULT_MIN_SCALE;
static MAX_SCALE = DEFAULT_MAX_SCALE;
static EMPTY_PERCENTAGE_OBJECT = { value: "", scale: "" };
protected static validate(values: PercentageProps) {
const schema = z.object({
value: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."),

View File

@ -1,6 +1,8 @@
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2;
@ -13,6 +15,8 @@ export interface QuantityProps {
}
export class Quantity extends ValueObject<QuantityProps> {
static EMPTY_QUANTITY_OBJECT = { value: "", scale: "" };
protected static validate(values: QuantityProps) {
const schema = z.object({
value: z.number().int(),