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, IssuedInvoiceFullSnapshotBuilder,
IssuedInvoiceItemsFullSnapshotBuilder, IssuedInvoiceItemsFullSnapshotBuilder,
IssuedInvoiceRecipientFullSnapshotBuilder, IssuedInvoiceRecipientFullSnapshotBuilder,
IssuedInvoiceTaxesFullSnapshotBuilder,
IssuedInvoiceVerifactuFullSnapshotBuilder, IssuedInvoiceVerifactuFullSnapshotBuilder,
} from "../snapshot-builders/full"; } from "../snapshot-builders/full";
import { import {
@ -16,6 +17,8 @@ import {
export function buildIssuedInvoiceSnapshotBuilders() { export function buildIssuedInvoiceSnapshotBuilders() {
const itemsBuilder = new IssuedInvoiceItemsFullSnapshotBuilder(); const itemsBuilder = new IssuedInvoiceItemsFullSnapshotBuilder();
const taxesBuilder = new IssuedInvoiceTaxesFullSnapshotBuilder();
const recipientBuilder = new IssuedInvoiceRecipientFullSnapshotBuilder(); const recipientBuilder = new IssuedInvoiceRecipientFullSnapshotBuilder();
const verifactuBuilder = new IssuedInvoiceVerifactuFullSnapshotBuilder(); const verifactuBuilder = new IssuedInvoiceVerifactuFullSnapshotBuilder();
@ -23,7 +26,8 @@ export function buildIssuedInvoiceSnapshotBuilders() {
const fullSnapshotBuilder = new IssuedInvoiceFullSnapshotBuilder( const fullSnapshotBuilder = new IssuedInvoiceFullSnapshotBuilder(
itemsBuilder, itemsBuilder,
recipientBuilder, recipientBuilder,
verifactuBuilder verifactuBuilder,
taxesBuilder
); );
const listSnapshotBuilder = new IssuedInvoiceListItemSnapshotBuilder(); 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-full-snapshot-builder";
export * from "./issued-invoice-item-full-snapshot.interface"; export * from "./issued-invoice-item-full-snapshot.interface";
export * from "./issued-invoice-items-full-snapshot-builder"; 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-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.interface";
export * from "./issued-invoice-verifactu-full-snapshot-builder"; export * from "./issued-invoice-verifactu-full-snapshot-builder";

View File

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

View File

@ -1,5 +1,6 @@
import type { IIssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot.interface"; 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"; import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface";
export interface IIssuedInvoiceFullSnapshot { export interface IIssuedInvoiceFullSnapshot {
@ -30,10 +31,11 @@ export interface IIssuedInvoiceFullSnapshot {
}; };
subtotal_amount: { value: string; scale: string; currency_code: string }; 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 }; items_discount_amount: { value: string; scale: string; currency_code: 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 }; 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 }; taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string }; total_amount: { value: string; scale: string; currency_code: string };
taxes: Array<{ taxes: 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 };
}>;
verifactu: IIssuedInvoiceVerifactuFullSnapshot; verifactu: IIssuedInvoiceVerifactuFullSnapshot;
items: IIssuedInvoiceItemFullSnapshot[]; items: IIssuedInvoiceItemFullSnapshot[];

View File

@ -1,5 +1,10 @@
import type { ISnapshotBuilder } from "@erp/core/api"; 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"; import type { IssuedInvoiceItem, IssuedInvoiceItems } from "../../../../domain";
@ -16,75 +21,38 @@ export class IssuedInvoiceItemsFullSnapshotBuilder
id: invoiceItem.id.toPrimitive(), id: invoiceItem.id.toPrimitive(),
is_valued: String(invoiceItem.isValued), is_valued: String(invoiceItem.isValued),
position: String(index), position: String(index),
description: maybeToEmptyString(invoiceItem.description, (value) => value.toPrimitive()),
quantity: invoiceItem.quantity.match( description: maybeToEmptyString(invoiceItem.description, (value) => value.toString()),
(quantity) => quantity.toObjectString(),
() => ({ value: "", scale: "" }) 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( taxable_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxableAmount),
(unitAmount) => unitAmount.toObjectString(),
() => ({ value: "", scale: "", currency_code: "" })
),
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( rec_code: maybeToEmptyString(invoiceItem.recCode),
(discountPercentage) => discountPercentage.toObjectString(), rec_percentage: maybeToEmptyPercentageObjectString(invoiceItem.recPercentage),
() => ({ value: "", scale: "" }) 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( taxes_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxesAmount),
(discountPercentage) => discountPercentage.toObjectString(), total_amount: maybeToEmptyMoneyObjectString(invoiceItem.totalAmount),
() => ({ 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(),
}; };
} }

View File

@ -3,7 +3,7 @@ import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd";
import type { InvoiceRecipient, IssuedInvoice } from "../../../../domain"; 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 export interface IIssuedInvoiceRecipientFullSnapshotBuilder
extends ISnapshotBuilder<IssuedInvoice, IIssuedInvoiceRecipientFullSnapshot> {} 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 "./application-models";
export * from "./di"; export * from "./di";
export * from "./dtos"; export * from "./dtos";
export * from "./mappers"; //export * from "./mappers";
export * from "./repositories"; export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount, ItemDiscountPercentage } from "../../../common"; import { ItemDiscountPercentage } from "../../../common";
import type { IssuedInvoiceItem } from "./issued-invoice-item.entity"; import type { IssuedInvoiceItem } from "./issued-invoice-item.entity";
@ -57,10 +57,10 @@ export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> {
return super.add(item); return super.add(item);
} }
public getTotalAmount(): InvoiceAmount { /*public getTotalAmount(): InvoiceAmount {
return this.getAll().reduce( return this.getAll().reduce(
(acc, item) => acc.add(item.getProps().totalAmount), (acc, item) => acc.add(item.getProps().totalAmount),
InvoiceAmount.zero(this.getAll()[0]?.getProps().totalAmount.currencyCode ?? "EUR") 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 { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../common"; import type { InvoiceAmount } from "../../../common";
export type IssuedInvoiceTaxProps = { export type IssuedInvoiceTaxProps = {
taxableAmount: InvoiceAmount; 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_value: number;
declare taxable_amount_scale: 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 // Total taxes amount / taxes total
declare taxes_amount_value: number; declare taxes_amount_value: number;
declare taxes_amount_scale: number; declare taxes_amount_scale: number;
@ -350,6 +362,40 @@ export default (database: Sequelize) => {
defaultValue: 2, 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: { taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,

View File

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

View File

@ -339,10 +339,6 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.subtotalAmount, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.subtotalAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE, ItemAmount.DEFAULT_SCALE,
// Te has quedado aquí --- IGNORE ---
// !!!!!!!!!!!!!!!!!!!
//
discount_percentage_value: maybeToNullable( discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage, source.itemDiscountPercentage,
(v) => v.toPrimitive().value (v) => v.toPrimitive().value
@ -351,29 +347,46 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, ItemDiscountPercentage.DEFAULT_SCALE,
discount_amount_value: source.itemDiscountAmount.value, discount_amount_value: maybeToNullable(
discount_amount_scale: source.itemDiscountAmount.scale, source.itemDiscountAmount,
(v) => v.toPrimitive().value
),
discount_amount_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE,
//
global_discount_percentage_value: maybeToNullable( global_discount_percentage_value: maybeToNullable(
source.globalDiscountPercentage, source.globalDiscountPercentage,
(v) => v.toPrimitive().value (v) => v.toPrimitive().value
), ),
global_discount_percentage_scale: global_discount_percentage_scale:
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, ItemDiscountPercentage.DEFAULT_SCALE,
global_discount_amount_value: source.globalDiscountAmount.value, global_discount_amount_value: maybeToNullable(
global_discount_amount_scale: source.globalDiscountAmount.scale, 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, taxable_amount_value: maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().value),
total_discount_amount_scale: source.totalDiscountAmount.scale, taxable_amount_scale:
maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().scale) ??
// ItemAmount.DEFAULT_SCALE,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
// IVA // IVA
iva_code: maybeToNullableString(source.ivaCode), iva_code: maybeToNullableString(source.ivaCode),
@ -410,12 +423,16 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4, maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4,
// //
taxes_amount_value: source.taxesAmount.value, taxes_amount_value: maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().value),
taxes_amount_scale: source.taxesAmount.scale, taxes_amount_scale:
maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
// //
total_amount_value: source.totalAmount.value, total_amount_value: maybeToNullable(source.totalAmount, (v) => v.toPrimitive().value),
total_amount_scale: source.totalAmount.scale, 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, subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema, items_discount_amount: MoneySchema,
discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
discount_amount: MoneySchema, global_discount_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema, taxable_amount: MoneySchema,
iva_amount: MoneySchema, iva_amount: MoneySchema,
rec_amount: MoneySchema, rec_amount: MoneySchema,

View File

@ -1,8 +1,9 @@
// application/shared/normalizers.ts
// Normalizadores y adaptadores DTO -> Maybe/VO // Normalizadores y adaptadores DTO -> Maybe/VO
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { MoneyValue, Percentage, Quantity } from "../value-objects";
/** any | null | undefined -> Maybe<T> usando validación */ /** any | null | undefined -> Maybe<T> usando validación */
export function maybeFromNullableResult<T>( export function maybeFromNullableResult<T>(
input: any, input: any,
@ -13,6 +14,115 @@ export function maybeFromNullableResult<T>(
return value.isSuccess ? Result.ok(Maybe.some(value.data)) : Result.fail(value.error); 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) */ /** string | null | undefined -> Maybe<string> (trim, vacío => None) */
export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<string> { export function maybeFromNullableOrEmptyString(input?: string | null): Maybe<string> {
if (isNullishOrEmpty(input)) return Maybe.none<string>(); if (isNullishOrEmpty(input)) return Maybe.none<string>();

View File

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

View File

@ -26,6 +26,8 @@ export class Percentage extends ValueObject<PercentageProps> {
static MIN_SCALE = DEFAULT_MIN_SCALE; static MIN_SCALE = DEFAULT_MIN_SCALE;
static MAX_SCALE = DEFAULT_MAX_SCALE; static MAX_SCALE = DEFAULT_MAX_SCALE;
static EMPTY_PERCENTAGE_OBJECT = { value: "", scale: "" };
protected static validate(values: PercentageProps) { protected static validate(values: PercentageProps) {
const schema = z.object({ const schema = z.object({
value: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."), 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 { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers"; import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object"; import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2; const DEFAULT_SCALE = 2;
@ -13,6 +15,8 @@ export interface QuantityProps {
} }
export class Quantity extends ValueObject<QuantityProps> { export class Quantity extends ValueObject<QuantityProps> {
static EMPTY_QUANTITY_OBJECT = { value: "", scale: "" };
protected static validate(values: QuantityProps) { protected static validate(values: QuantityProps) {
const schema = z.object({ const schema = z.object({
value: z.number().int(), value: z.number().int(),