diff --git a/modules/core/src/api/domain/value-objects/index.ts b/modules/core/src/api/domain/value-objects/index.ts index 41ca139d..c22ae634 100644 --- a/modules/core/src/api/domain/value-objects/index.ts +++ b/modules/core/src/api/domain/value-objects/index.ts @@ -1,2 +1 @@ export * from "./tax"; -export * from "./taxes"; diff --git a/modules/core/src/api/domain/value-objects/taxes.ts b/modules/core/src/api/domain/value-objects/taxes.ts index 71c7bbc0..53eedcf7 100644 --- a/modules/core/src/api/domain/value-objects/taxes.ts +++ b/modules/core/src/api/domain/value-objects/taxes.ts @@ -1,8 +1,9 @@ import { Collection } from "@repo/rdx-utils"; -import { Tax } from "./tax"; + +import type { Tax } from "./tax"; export class Taxes extends Collection { public static create(this: new (items: Tax[]) => T, items: Tax[]): T { - return new this(items); + return new Taxes(items); } } diff --git a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts b/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts index 439b7292..4deb41a5 100644 --- a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts +++ b/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts @@ -58,7 +58,7 @@ export function mapDTOToCustomerInvoiceItemsProps( description: description, quantity: quantity, unitAmount: unitAmount, - discountPercentage: discountPercentage, + itemDiscountPercentage: discountPercentage, //currencyCode, //languageCode, //taxes: diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts index 4ff1084b..3bba23e8 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice-items.full.presenter.ts @@ -14,7 +14,7 @@ export class IssuedInvoiceItemsFullPresenter extends Presenter { invoiceItem: CustomerInvoiceItem, index: number ): GetIssuedInvoiceItemByInvoiceIdResponseDTO { - const allAmounts = invoiceItem.getAllAmounts(); + const allAmounts = invoiceItem.calculateAllAmounts(); return { id: invoiceItem.id.toPrimitive(), @@ -34,15 +34,58 @@ export class IssuedInvoiceItemsFullPresenter extends Presenter { subtotal_amount: allAmounts.subtotalAmount.toObjectString(), - discount_percentage: invoiceItem.discountPercentage.match( + discount_percentage: invoiceItem.itemDiscountPercentage.match( (discountPercentage) => discountPercentage.toObjectString(), () => ({ value: "", scale: "" }) ), - discount_amount: allAmounts.discountAmount.toObjectString(), + discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + + global_discount_percentage: invoiceItem.globalDiscountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + global_discount_amount: allAmounts.globalDiscountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), - tax_codes: invoiceItem.taxes.getCodesArray(), + + iva_code: invoiceItem.taxes.iva.match( + (iva) => iva.code, + () => "" + ), + + iva_percentage: invoiceItem.taxes.iva.match( + (iva) => iva.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + iva_amount: allAmounts.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: allAmounts.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: allAmounts.retentionAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts index 2eef7f79..777c97e2 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/issued-invoices/issued-invoice.full.presenter.ts @@ -2,7 +2,7 @@ import { Presenter } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto"; -import type { CustomerInvoice } from "../../../../domain"; +import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; import type { IssuedInvoiceItemsFullPresenter } from "./issued-invoice-items.full.presenter"; import type { IssuedInvoiceRecipientFullPresenter } from "./issued-invoice-recipient.full.presenter"; @@ -31,7 +31,7 @@ export class IssuedInvoiceFullPresenter extends Presenter< const recipient = recipientPresenter.toOutput(invoice); const items = itemsPresenter.toOutput(invoice.items); const verifactu = verifactuPresenter.toOutput(invoice); - const allAmounts = invoice.getAllAmounts(); + const allAmounts = invoice.calculateAllAmounts(); const payment = invoice.paymentMethod.match( (payment) => { @@ -44,11 +44,49 @@ export class IssuedInvoiceFullPresenter extends Presenter< () => undefined ); - const invoiceTaxes = invoice.getTaxes().map((taxItem) => { + 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 { - tax_code: taxItem.tax.code, - taxable_amount: taxItem.taxableAmount.toObjectString(), - taxes_amount: taxItem.taxesAmount.toObjectString(), + 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(), }; }); @@ -74,20 +112,25 @@ export class IssuedInvoiceFullPresenter extends Presenter< customer_id: invoice.customerId.toString(), recipient, - taxes: invoiceTaxes, - payment_method: payment, subtotal_amount: allAmounts.subtotalAmount.toObjectString(), items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(), discount_percentage: invoice.discountPercentage.toObjectString(), - discount_amount: allAmounts.headerDiscountAmount.toObjectString(), + discount_amount: allAmounts.globalDiscountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), + + iva_amount: totalIvaAmount.toObjectString(), + rec_amount: totalRecAmount.toObjectString(), + retention_amount: totalRetentionAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), + taxes: invoiceTaxes, + verifactu, items, diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts index 58110282..26bfb6a9 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma-items.full.presenter.ts @@ -12,7 +12,7 @@ export class ProformaItemsFullPresenter extends Presenter { proformaItem: CustomerInvoiceItem, index: number ): GetProformaItemByIdResponseDTO { - const allAmounts = proformaItem.getAllAmounts(); + const allAmounts = proformaItem.calculateAllAmounts(); return { id: proformaItem.id.toPrimitive(), @@ -32,15 +32,58 @@ export class ProformaItemsFullPresenter extends Presenter { subtotal_amount: allAmounts.subtotalAmount.toObjectString(), - discount_percentage: proformaItem.discountPercentage.match( + discount_percentage: proformaItem.itemDiscountPercentage.match( (discountPercentage) => discountPercentage.toObjectString(), () => ({ value: "", scale: "" }) ), - discount_amount: allAmounts.discountAmount.toObjectString(), + discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + + global_discount_percentage: proformaItem.globalDiscountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + global_discount_amount: allAmounts.globalDiscountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), - tax_codes: proformaItem.taxes.getCodesArray(), + + iva_code: proformaItem.taxes.iva.match( + (iva) => iva.code, + () => "" + ), + + iva_percentage: proformaItem.taxes.iva.match( + (iva) => iva.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + iva_amount: allAmounts.ivaAmount.toObjectString(), + + rec_code: proformaItem.taxes.rec.match( + (rec) => rec.code, + () => "" + ), + + rec_percentage: proformaItem.taxes.rec.match( + (rec) => rec.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + rec_amount: allAmounts.recAmount.toObjectString(), + + retention_code: proformaItem.taxes.retention.match( + (retention) => retention.code, + () => "" + ), + + retention_percentage: proformaItem.taxes.retention.match( + (retention) => retention.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + retention_amount: allAmounts.retentionAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts index 37ee2341..03af9c01 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/proformas/proforma.full.presenter.ts @@ -2,7 +2,7 @@ import { Presenter } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; import type { GetProformaByIdResponseDTO } from "../../../../../common/dto"; -import type { CustomerInvoice } from "../../../../domain"; +import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; import type { ProformaItemsFullPresenter } from "./proforma-items.full.presenter"; import type { ProformaRecipientFullPresenter } from "./proforma-recipient.full.presenter"; @@ -21,7 +21,7 @@ export class ProformaFullPresenter extends Presenter { @@ -34,11 +34,49 @@ export class ProformaFullPresenter extends Presenter undefined ); - const invoiceTaxes = proforma.getTaxes().map((taxItem) => { + let totalIvaAmount = InvoiceAmount.zero(proforma.currencyCode.code); + let totalRecAmount = InvoiceAmount.zero(proforma.currencyCode.code); + let totalRetentionAmount = InvoiceAmount.zero(proforma.currencyCode.code); + + const invoiceTaxes = proforma.getTaxes().map((taxGroup) => { + const { ivaAmount, recAmount, retentionAmount, totalAmount } = taxGroup.calculateAmounts(); + + totalIvaAmount = totalIvaAmount.add(ivaAmount); + totalRecAmount = totalRecAmount.add(recAmount); + totalRetentionAmount = totalRetentionAmount.add(retentionAmount); + return { - tax_code: taxItem.tax.code, - taxable_amount: taxItem.taxableAmount.toObjectString(), - taxes_amount: taxItem.taxesAmount.toObjectString(), + 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(), }; }); @@ -72,9 +110,14 @@ export class ProformaFullPresenter extends Presenter ({ - tax_code: t.tax_code, - taxable_amount: t.taxable_amount.toObjectString(), - taxes_amount: t.taxes_amount.toObjectString(), - })), - subtotal_amount: invoice.subtotalAmount.toObjectString(), discount_percentage: invoice.discountPercentage.toObjectString(), discount_amount: invoice.discountAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts index e36872f2..aabb6cab 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/proformas/proforma.list.presenter.ts @@ -30,12 +30,6 @@ export class ProformaListPresenter extends Presenter { language_code: proforma.languageCode.code, currency_code: proforma.currencyCode.code, - taxes: proforma.taxes.map((t) => ({ - tax_code: t.tax_code, - taxable_amount: t.taxable_amount.toObjectString(), - taxes_amount: t.taxes_amount.toObjectString(), - })), - subtotal_amount: proforma.subtotalAmount.toObjectString(), discount_percentage: proforma.discountPercentage.toObjectString(), discount_amount: proforma.discountAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts index 49d53e9d..91586897 100644 --- a/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/issued-invoices/issued-invoice-taxes.report.presenter.ts @@ -1,4 +1,9 @@ -import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core"; +import { + type JsonTaxCatalogProvider, + MoneyDTOHelper, + PercentageDTOHelper, + SpainTaxCatalogProvider, +} from "@erp/core"; import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { ArrayElement } from "@repo/rdx-utils"; @@ -16,17 +21,23 @@ export class IssuedInvoiceTaxesReportPresenter extends Presenter item.name, - () => taxItem.tax_code // fallback - ); + //const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code); return { - tax_code: taxItem.tax_code, - tax_name: taxName, taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions), + + iva_code: taxItem.iva_code, + iva_percentage: PercentageDTOHelper.format(taxItem.iva_percentage, this._locale), + iva_amount: MoneyDTOHelper.format(taxItem.iva_amount, this._locale, moneyOptions), + + rec_code: taxItem.rec_code, + rec_percentage: PercentageDTOHelper.format(taxItem.rec_percentage, this._locale), + rec_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions), + + retention_code: taxItem.retention_code, + retention_percentage: PercentageDTOHelper.format(taxItem.retention_percentage, this._locale), + retention_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions), }; } diff --git a/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts index 7f87816c..36b85a62 100644 --- a/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/reports/proformas/proforma-taxes.report.presenter.ts @@ -1,4 +1,9 @@ -import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core"; +import { + type JsonTaxCatalogProvider, + MoneyDTOHelper, + PercentageDTOHelper, + SpainTaxCatalogProvider, +} from "@erp/core"; import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; import type { ArrayElement } from "@repo/rdx-utils"; @@ -16,17 +21,23 @@ export class ProformaTaxesReportPresenter extends Presenter item.name, - () => taxItem.tax_code // fallback - ); + //const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code); return { - tax_code: taxItem.tax_code, - tax_name: taxName, taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions), + + iva_code: taxItem.iva_code, + iva_percentage: PercentageDTOHelper.format(taxItem.iva_percentage, this._locale), + iva_amount: MoneyDTOHelper.format(taxItem.iva_amount, this._locale, moneyOptions), + + rec_code: taxItem.rec_code, + rec_percentage: PercentageDTOHelper.format(taxItem.rec_percentage, this._locale), + rec_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions), + + retention_code: taxItem.retention_code, + retention_percentage: PercentageDTOHelper.format(taxItem.retention_percentage, this._locale), + retention_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions), + taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions), }; } diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts index 1efc0b89..44c4bbee 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts @@ -227,7 +227,7 @@ export class CreateCustomerInvoicePropsMapper { description: description!, quantity: quantity!, unitAmount: unitAmount!, - discountPercentage: discountPercentage!, + itemDiscountPercentage: discountPercentage!, taxes: taxes, }; diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index c71be482..dde55239 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -8,20 +8,17 @@ import { type UniqueID, type UtcDate, } from "@repo/rdx-ddd"; -import { type Maybe, Result } from "@repo/rdx-utils"; +import { Collection, type Maybe, Result } from "@repo/rdx-utils"; -import { - CustomerInvoiceItems, - type InvoicePaymentMethod, - type InvoiceTaxTotal, - type VerifactuRecord, -} from "../entities"; +import { CustomerInvoiceItems, type InvoicePaymentMethod, type VerifactuRecord } from "../entities"; import { type CustomerInvoiceNumber, type CustomerInvoiceSerie, type CustomerInvoiceStatus, InvoiceAmount, type InvoiceRecipient, + type InvoiceTaxGroup, + type ItemAmount, } from "../value-objects"; export interface CustomerInvoiceProps { @@ -69,7 +66,7 @@ export interface ICustomerInvoice { hasRecipient: boolean; hasPaymentMethod: boolean; - getTaxes(): InvoiceTaxTotal[]; + getTaxes(): Collection; getProps(): CustomerInvoiceProps; } @@ -86,25 +83,10 @@ export class CustomerInvoice CustomerInvoiceItems.create({ languageCode: props.languageCode, currencyCode: props.currencyCode, + globalDiscountPercentage: props.discountPercentage, }); } - getHeaderDiscountAmount(): InvoiceAmount { - throw new Error("Method not implemented."); - } - - getTaxableAmount(): InvoiceAmount { - throw new Error("Method not implemented."); - } - - getTaxesAmount(): InvoiceAmount { - throw new Error("Method not implemented."); - } - - getTotalAmount(): InvoiceAmount { - throw new Error("Method not implemented."); - } - static create(props: CustomerInvoiceProps, id?: UniqueID): Result { const customerInvoice = new CustomerInvoice(props, id); @@ -127,25 +109,7 @@ export class CustomerInvoice return Result.ok(customerInvoice); } - public update(partialInvoice: CustomerInvoicePatchProps): Result { - const { items, ...rest } = partialInvoice; - - const updatedProps = { - ...this.props, - ...rest, - } as CustomerInvoiceProps; - - /*if (partialAddress) { - const updatedAddressOrError = this.address.update(partialAddress); - if (updatedAddressOrError.isFailure) { - return Result.fail(updatedAddressOrError.error); - } - - updatedProps.address = updatedAddressOrError.data; - }*/ - - return CustomerInvoice.create(updatedProps, this.id); - } + // Getters public get companyId(): UniqueID { return this.props.companyId; @@ -236,65 +200,123 @@ export class CustomerInvoice return this.paymentMethod.isSome(); } - /* CALCULOS INTERNOS */ - - private _getSubtotalAmount(): InvoiceAmount { - const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2); + // Helpers + /** + * @summary Convierte un ItemAmount a InvoiceAmount (mantiene moneda y escala homogénea). + */ + private _toInvoiceAmount(itemAmount: ItemAmount): InvoiceAmount { return InvoiceAmount.create({ - value: itemsSubtotal.value, + value: itemAmount.convertScale(InvoiceAmount.DEFAULT_SCALE).value, currency_code: this.currencyCode.code, }).data; } - private _getItemsDiscountAmount(): InvoiceAmount { - const itemsDiscountAmount = this.items.getDiscountAmount().convertScale(2); + // Cálculos - return InvoiceAmount.create({ - value: itemsDiscountAmount.value, - currency_code: this.currencyCode.code, - }).data; + /** + * @summary Calcula todos los totales de factura a partir de los totales de las líneas. + * La cabecera NO recalcula lógica de porcentaje — toda la lógica está en Item/Items. + */ + public calculateAllAmounts() { + const itemsTotals = this.items.calculateAllAmounts(); + + const subtotalAmount = this._toInvoiceAmount(itemsTotals.subtotalAmount); + + const itemDiscountAmount = this._toInvoiceAmount(itemsTotals.itemDiscountAmount); + const globalDiscountAmount = this._toInvoiceAmount(itemsTotals.globalDiscountAmount); + const totalDiscountAmount = this._toInvoiceAmount(itemsTotals.totalDiscountAmount); + + const taxableAmount = this._toInvoiceAmount(itemsTotals.taxableAmount); + const taxesAmount = this._toInvoiceAmount(itemsTotals.taxesAmount); + const totalAmount = this._toInvoiceAmount(itemsTotals.totalAmount); + + return { + subtotalAmount, + itemDiscountAmount, + globalDiscountAmount, + totalDiscountAmount, + taxableAmount, + taxesAmount, + totalAmount, + } as const; } - private _getHeaderDiscountAmount( - subtotalAmount: InvoiceAmount, - itemsDiscountAmount: InvoiceAmount - ): InvoiceAmount { - return subtotalAmount.subtract(itemsDiscountAmount).percentage(this.discountPercentage); + // Métodos públicos + + public getProps(): CustomerInvoiceProps { + return this.props; } - private _getTaxableAmount( - subtotalAmount: InvoiceAmount, - itemsDiscountAmount: InvoiceAmount, - headerDiscountAmount: InvoiceAmount - ): InvoiceAmount { - return subtotalAmount.subtract(itemsDiscountAmount).subtract(headerDiscountAmount); + public update(partialInvoice: CustomerInvoicePatchProps): Result { + const { items, ...rest } = partialInvoice; + + const updatedProps = { + ...this.props, + ...rest, + } as CustomerInvoiceProps; + + /*if (partialAddress) { + const updatedAddressOrError = this.address.update(partialAddress); + if (updatedAddressOrError.isFailure) { + return Result.fail(updatedAddressOrError.error); + } + + updatedProps.address = updatedAddressOrError.data; + }*/ + + return CustomerInvoice.create(updatedProps, this.id); } - // total impuestos suma(iva + rec + retenciones) - private _getTaxesAmount(): InvoiceAmount { - const { iva, rec, retention } = this.items.getAggregatedTaxesByType(); - const total = iva.add(rec).add(retention); - return InvoiceAmount.create({ - value: total.convertScale(2).value, - currency_code: this.currencyCode.code, - }).data; + public getSubtotalAmount(): InvoiceAmount { + return this.calculateAllAmounts().subtotalAmount; } - private _getTotalAmount(taxableAmount: InvoiceAmount, taxesAmount: InvoiceAmount): InvoiceAmount { - return taxableAmount.add(taxesAmount); + public getItemDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().itemDiscountAmount; } - /** Totales expuestos */ + public getGlobalDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().globalDiscountAmount; + } - public getTaxes(): InvoiceTaxTotal[] { - const map = this.items.getAggregatedTaxesByCode(); + public getTotalDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().totalDiscountAmount; + } + + public getTaxableAmount(): InvoiceAmount { + return this.calculateAllAmounts().taxableAmount; + } + + public getTaxesAmount(): InvoiceAmount { + return this.calculateAllAmounts().taxesAmount; + } + + public getTotalAmount(): InvoiceAmount { + return this.calculateAllAmounts().totalAmount; + } + + /** + * @summary Devuelve la agrupación de impuestos útil para poblar `customer_invoice_taxes`. + */ + public getTaxesGroupedByCode() { + return this.items.groupTaxesByCode(); + } + + public getTaxes(): Collection { + const map = this.items.groupTaxesByCode(); const currency = this.currencyCode.code; - const result: InvoiceTaxTotal[] = []; + const result = new Collection([]); for (const [tax_code, entry] of map.entries()) { - result.push({ + const value: InvoiceTaxGroup = { + ta, + }; + + result.push(value); + + /*result.push({ tax: entry.tax, taxableAmount: InvoiceAmount.create({ value: entry.taxable.convertScale(2).value, @@ -304,37 +326,9 @@ export class CustomerInvoice value: entry.total.convertScale(2).value, currency_code: currency, }).data, - }); + });*/ } - return result; - } - - public getAllAmounts() { - const subtotalAmount = this._getSubtotalAmount(); // Sin IVA ni dtos de línea - const itemDiscountAmount = this._getItemsDiscountAmount(); // Suma de los Importes de descuentos de linea - const headerDiscountAmount = this._getHeaderDiscountAmount(subtotalAmount, itemDiscountAmount); // Importe de descuento de cabecera - - const taxableAmount = this._getTaxableAmount( - subtotalAmount, - itemDiscountAmount, - headerDiscountAmount - ); // - - const taxesAmount = this._getTaxesAmount(); - const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount); - - return { - subtotalAmount, - itemDiscountAmount, - headerDiscountAmount, - taxableAmount, - taxesAmount, - totalAmount, - }; - } - - public getProps(): CustomerInvoiceProps { - return this.props; + return new Collection(result); } } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts index d01e0b60..aef308b1 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts @@ -9,12 +9,30 @@ import { } from "../../value-objects"; import type { ItemTaxGroup } from "../../value-objects/item-tax-group"; +/** + * + * Entidad de línea de factura. + * + * Modela: + * - subtotal = cantidad × precio + * - descuento de línea + * - descuento global (prorrateado proporcionalmente desde cabecera) + * - base imponible + * - impuestos + * - total final + * + * Esta entidad es inmutable en comportamiento: todos los importes se calculan + * en tiempo real a partir de las propiedades (cantidad, precio y porcentajes). + * + */ + export interface CustomerInvoiceItemProps { description: Maybe; quantity: Maybe; // Cantidad de unidades unitAmount: Maybe; // Precio unitario en la moneda de la factura - discountPercentage: Maybe; // % descuento + itemDiscountPercentage: Maybe; // % descuento + globalDiscountPercentage: Maybe; // % descuento de la cabecera taxes: ItemTaxGroup; @@ -22,33 +40,7 @@ export interface CustomerInvoiceItemProps { currencyCode: CurrencyCode; } -export interface ICustomerInvoiceItem { - isValued: boolean; - - description: Maybe; - - quantity: Maybe; // Cantidad de unidades - unitAmount: Maybe; // Precio unitario en la moneda de la factura - - discountPercentage: Maybe; // % descuento - - taxes: ItemTaxGroup; - - languageCode: LanguageCode; - currencyCode: CurrencyCode; - - getSubtotalAmount(): ItemAmount; - getDiscountAmount(): ItemAmount; - - getTaxableAmount(): ItemAmount; - getTaxesAmount(): ItemAmount; - getTotalAmount(): ItemAmount; -} - -export class CustomerInvoiceItem - extends DomainEntity - implements ICustomerInvoiceItem -{ +export class CustomerInvoiceItem extends DomainEntity { protected _isValued!: boolean; public static create( @@ -70,6 +62,8 @@ export class CustomerInvoiceItem this._isValued = this.quantity.isSome() || this.unitAmount.isSome(); } + // Getters + get isValued(): boolean { return this._isValued; } @@ -77,24 +71,34 @@ export class CustomerInvoiceItem get description() { return this.props.description; } + get quantity() { return this.props.quantity; } + get unitAmount() { return this.props.unitAmount; } - get discountPercentage() { - return this.props.discountPercentage; + + get itemDiscountPercentage() { + return this.props.itemDiscountPercentage; } + + get globalDiscountPercentage() { + return this.props.globalDiscountPercentage; + } + + get taxes() { + return this.props.taxes; + } + get languageCode() { return this.props.languageCode; } + get currencyCode() { return this.props.currencyCode; } - get taxes() { - return this.props.taxes; - } getProps(): CustomerInvoiceItemProps { return this.props; @@ -104,65 +108,12 @@ export class CustomerInvoiceItem return this.getProps(); } - /** - * @private - * @summary Calcula el importe de descuento a partir del subtotal y el porcentaje. - * @param subtotalAmount - Importe subtotal. - * @returns El importe de descuento calculado. - */ - private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount { - const discount = this.discountPercentage.match( - (discount) => discount, - () => ItemDiscount.zero() - ); - return subtotalAmount.percentage(discount); - } + // Ayudantes /** - * @private - * @summary Calcula el importe imponible restando el descuento al subtotal. - * @param subtotalAmount - Importe subtotal. - * @param discountAmount - Importe de descuento. - * @returns El importe imponible resultante. + * @summary Helper puro para calcular el subtotal. */ - private _getTaxableAmount(subtotalAmount: ItemAmount, discountAmount: ItemAmount): ItemAmount { - return subtotalAmount.subtract(discountAmount); - } - - /* importes individuales: iva / rec / ret */ - private _getIndividualTaxAmounts(taxableAmount: ItemAmount) { - return this.props.taxes.calculateAmounts(taxableAmount); - } - - /** - * @private - * @summary Calcula el importe total de impuestos sobre la base imponible. - * @param taxableAmount - Importe imponible. - * @returns El importe de impuestos calculado. - */ - private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { - const { ivaAmount, recAmount, retentionAmount } = this._getIndividualTaxAmounts(taxableAmount); - return ivaAmount.add(recAmount).add(retentionAmount); // retención ya es negativa - } - - /** - * @private - * @summary Calcula el importe total sumando base imponible e impuestos. - * @param taxableAmount - Importe imponible. - * @param taxesAmount - Importe de impuestos. - * @returns El importe total del ítem. - */ - private _getTotalAmount(taxableAmount: ItemAmount, taxesAmount: ItemAmount): ItemAmount { - return taxableAmount.add(taxesAmount); - } - - /** - * @summary Calcula el subtotal del ítem (cantidad × importe unitario). - * @returns Un `ItemAmount` con el subtotal del ítem. - * @remarks - * Si la cantidad o el importe unitario no están definidos, se asumen valores cero. - */ - public getSubtotalAmount(): ItemAmount { + private _calculateSubtotalAmount(): ItemAmount { const qty = this.quantity.match( (quantity) => quantity, () => ItemQuantity.zero() @@ -175,70 +126,137 @@ export class CustomerInvoiceItem } /** - * @summary Calcula el importe total de descuento del ítem. - * @returns Un `ItemAmount` con el importe descontado. + * @summary Helper puro para calcular el descuento de línea. */ - public getDiscountAmount(): ItemAmount { - return this._getDiscountAmount(this.getSubtotalAmount()); + private _calculateItemDiscountAmount(subtotal: ItemAmount): ItemAmount { + const discountPercentage = this.props.itemDiscountPercentage.match( + (discount) => discount, + () => ItemDiscount.zero() + ); + + return subtotal.percentage(discountPercentage); } /** - * @summary Calcula el importe imponible (subtotal − descuento). - * @returns Un `ItemAmount` con la base imponible del ítem. + * @summary Helper puro para calcular el descuento global. */ - public getTaxableAmount(): ItemAmount { - return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount()); - } + private _calculateGlobalDiscountAmount( + subtotalAmount: ItemAmount, + discountAmount: ItemAmount + ): ItemAmount { + const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount); - /* importes individuales: iva / rec / ret */ - public getIndividualTaxAmounts() { - return this._getIndividualTaxAmounts(this.getTaxableAmount()); + const globalDiscount = this.props.globalDiscountPercentage.match( + (discount) => discount, + () => ItemDiscount.zero() + ); + + return amountAfterLineDiscount.percentage(globalDiscount); } /** - * @summary Calcula el importe total de impuestos aplicados al ítem. - * @returns Un `ItemAmount` con el total de impuestos. + * @summary Helper puro para calcular la suma de descuentos. */ - public getTaxesAmount(): ItemAmount { - return this._getTaxesAmount(this.getTaxableAmount()); + + private _calculateTotalDiscountAmount( + itemDiscountAmount: ItemAmount, + globalDiscountAmount: ItemAmount + ) { + return itemDiscountAmount.add(globalDiscountAmount); } /** - * @summary Calcula el importe total final del ítem (base imponible + impuestos). - * @returns Un `ItemAmount` con el importe total. + * @summary Helper puro para calcular impuestos individuales. */ - public getTotalAmount(): ItemAmount { - const taxableAmount = this.getTaxableAmount(); - const taxesAmount = this._getTaxesAmount(taxableAmount); - - return this._getTotalAmount(taxableAmount, taxesAmount); + private _calculateIndividualTaxes(taxable: ItemAmount) { + return this.taxes.calculateAmounts(taxable); } + // Cálculos + /** - * @summary Devuelve todos los importes calculados del ítem en un único objeto. - * @returns Un objeto con las propiedades: - * - `subtotalAmount` - * - `discountAmount` - * - `taxableAmount` - * - `taxesAmount` - * - `totalAmount` - * @remarks - * Este método es útil para mostrar todos los cálculos en la interfaz de usuario - * o serializar el ítem con sus valores calculados. + * @summary Cálculo centralizado de todos los valores intermedios. + * @returns Devuelve un objeto inmutable con todos los valores necesarios: + * - subtotal + * - itemDiscount + * - globalDiscount + * - totalDiscount + * - taxableAmount + * - ivaAmount + * - recAmount + * - retentionAmount + * - taxesAmount + * - totalAmount + * */ - public getAllAmounts() { - const subtotalAmount = this.getSubtotalAmount(); - const discountAmount = this._getDiscountAmount(subtotalAmount); - const taxableAmount = this._getTaxableAmount(subtotalAmount, discountAmount); - const taxesAmount = this._getTaxesAmount(taxableAmount); - const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount); + public calculateAllAmounts() { + const subtotalAmount = this._calculateSubtotalAmount(); + + const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount); + const globalDiscountAmount = this._calculateGlobalDiscountAmount( + subtotalAmount, + itemDiscountAmount + ); + const totalDiscountAmount = this._calculateTotalDiscountAmount( + itemDiscountAmount, + globalDiscountAmount + ); + + const taxableAmount = subtotalAmount.subtract(totalDiscountAmount); + + const { ivaAmount, recAmount, retentionAmount } = this._calculateIndividualTaxes(taxableAmount); + + const taxesAmount = ivaAmount.add(recAmount).add(retentionAmount); + const totalAmount = taxableAmount.add(taxesAmount); return { subtotalAmount, - discountAmount, + + itemDiscountAmount, + globalDiscountAmount, + totalDiscountAmount, + taxableAmount, + + ivaAmount, + recAmount, + retentionAmount, + taxesAmount, totalAmount, - }; + } as const; + } + + public getSubtotalAmount(): ItemAmount { + return this.calculateAllAmounts().subtotalAmount; + } + + public getItemDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().itemDiscountAmount; + } + + public getGlobalDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().globalDiscountAmount; + } + + public getTotalDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().totalDiscountAmount; + } + + public getTaxableAmount(): ItemAmount { + return this.calculateAllAmounts().taxableAmount; + } + + public getIndividualTaxAmounts() { + const { ivaAmount, recAmount, retentionAmount } = this.calculateAllAmounts(); + return { ivaAmount, recAmount, retentionAmount }; + } + + public getTaxesAmount(): ItemAmount { + return this.calculateAllAmounts().taxesAmount; + } + + public getTotalAmount(): ItemAmount { + return this.calculateAllAmounts().totalAmount; } } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts index ebe26340..a856f1a7 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts @@ -1,8 +1,8 @@ import type { Tax } from "@erp/core/api"; -import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; +import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; -import { ItemAmount } from "../../value-objects"; +import { ItemAmount, ItemDiscount } from "../../value-objects"; import type { CustomerInvoiceItem } from "./customer-invoice-item"; @@ -10,101 +10,38 @@ export interface CustomerInvoiceItemsProps { items?: CustomerInvoiceItem[]; languageCode: LanguageCode; currencyCode: CurrencyCode; + globalDiscountPercentage: Percentage; } export class CustomerInvoiceItems extends Collection { private _languageCode!: LanguageCode; private _currencyCode!: CurrencyCode; + private _globalDiscountPercentage!: Percentage; constructor(props: CustomerInvoiceItemsProps) { super(props.items ?? []); this._languageCode = props.languageCode; this._currencyCode = props.currencyCode; + this._globalDiscountPercentage = props.globalDiscountPercentage; } public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { return new CustomerInvoiceItems(props); } - /** - * @summary Añade un nuevo ítem a la colección. - * @param item - El ítem de factura a añadir. - * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado. - * @remarks - * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con - * los de la colección. Si no coinciden, el método devuelve `false` sin modificar - * la colección. - */ - add(item: CustomerInvoiceItem): boolean { - // Antes de añadir un nuevo item, debo comprobar que el item a añadir - // tiene el mismo "currencyCode" y "languageCode" que la colección de items. - if ( - !( - this._languageCode.equals(item.languageCode) && this._currencyCode.equals(item.currencyCode) - ) - ) { - return false; - } - return super.add(item); - } + // Helpers - /** - * @summary Calcula el subtotal de todos los ítems sin descuentos ni impuestos. - * @returns Un `ItemAmount` con el subtotal total de la colección en su moneda. - */ - public getSubtotalAmount(): ItemAmount { + private _sumAmounts(selector: (item: CustomerInvoiceItem) => ItemAmount): ItemAmount { return this.getAll().reduce( - (total, item) => total.add(item.getSubtotalAmount()), + (acc, item) => acc.add(selector(item)), ItemAmount.zero(this._currencyCode.code) ); } /** - * @summary Calcula el importe total de descuentos aplicados a todos los ítems. - * @returns Un `ItemAmount` con el importe total de descuentos. + * @summary Helper puro para sumar impuestos individuales por tipo. */ - public getDiscountAmount(): ItemAmount { - return this.getAll().reduce( - (total, item) => total.add(item.getDiscountAmount()), - ItemAmount.zero(this._currencyCode.code) - ); - } - - /** - * @summary Calcula el importe imponible total (base antes de impuestos). - * @returns Un `ItemAmount` con el total imponible. - */ - public getTaxableAmount(): ItemAmount { - return this.getAll().reduce( - (total, item) => total.add(item.getTaxableAmount()), - ItemAmount.zero(this._currencyCode.code) - ); - } - - /** - * @summary Calcula el importe total de impuestos de todos los ítems. - * @returns Un `ItemAmount` con la suma de todos los impuestos aplicados. - */ - public getTaxesAmount(): ItemAmount { - return this.getAll().reduce( - (total, item) => total.add(item.getTaxesAmount()), - ItemAmount.zero(this._currencyCode.code) - ); - } - - /** - * @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos). - * @returns Un `ItemAmount` con el total global de la colección. - */ - public getTotalAmount(): ItemAmount { - return this.getAll().reduce( - (total, item) => total.add(item.getTotalAmount()), - ItemAmount.zero(this._currencyCode.code) - ); - } - - /* totales de iva/rec/ret a nivel factura */ - public getAggregatedTaxesByType() { + private _calculateIndividualTaxes() { let iva = ItemAmount.zero(this._currencyCode.code); let rec = ItemAmount.zero(this._currencyCode.code); let retention = ItemAmount.zero(this._currencyCode.code); @@ -120,21 +57,157 @@ export class CustomerInvoiceItems extends Collection { return { iva, rec, retention }; } - /* agrupación por código fiscal → usado para customer_invoice_taxes */ - public getAggregatedTaxesByCode() { + // + + /** + * @summary Añade un nuevo ítem a la colección. + * @param item - El ítem de factura a añadir. + * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado. + * @remarks + * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con + * los de la colección. Si no coinciden, el método devuelve `false` sin modificar + * la colección. + */ + add(item: CustomerInvoiceItem): boolean { + // Antes de añadir un nuevo item, debo comprobar que el item a añadir + // tiene el mismo "currencyCode" y "languageCode" que la colección de items. + if ( + !( + this._languageCode.equals(item.languageCode) && + this._currencyCode.equals(item.currencyCode) && + this._globalDiscountPercentage.equals( + item.globalDiscountPercentage.match( + (v) => v, + () => ItemDiscount.zero() + ) + ) + ) + ) { + return false; + } + return super.add(item); + } + + // Cálculos + + /** + * @summary Orquestador central del cálculo agregado de la colección. + * @remarks + * Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos. + */ + public calculateAllAmounts() { + let subtotalAmount = ItemAmount.zero(this._currencyCode.code); + + let itemDiscountAmount = ItemAmount.zero(this._currencyCode.code); + let globalDiscountAmount = ItemAmount.zero(this._currencyCode.code); + let totalDiscountAmount = ItemAmount.zero(this._currencyCode.code); + + let taxableAmount = ItemAmount.zero(this._currencyCode.code); + + let ivaAmount = ItemAmount.zero(this._currencyCode.code); + let recAmount = ItemAmount.zero(this._currencyCode.code); + let retentionAmount = ItemAmount.zero(this._currencyCode.code); + + let taxesAmount = ItemAmount.zero(this._currencyCode.code); + let totalAmount = ItemAmount.zero(this._currencyCode.code); + + for (const item of this.getAll()) { + const amounts = item.calculateAllAmounts(); + + // Subtotales + subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); + + // Descuentos + itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount); + globalDiscountAmount = globalDiscountAmount.add(amounts.globalDiscountAmount); + totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount); + + // Base imponible + taxableAmount = taxableAmount.add(amounts.taxableAmount); + + // Impuestos individuales + ivaAmount = ivaAmount.add(amounts.ivaAmount); + recAmount = recAmount.add(amounts.recAmount); + retentionAmount = retentionAmount.add(amounts.retentionAmount); + + // Total impuestos del ítem + taxesAmount = taxesAmount.add(amounts.taxesAmount); + + // Total final del ítem + totalAmount = totalAmount.add(amounts.totalAmount); + } + + return { + subtotalAmount, + + itemDiscountAmount, + globalDiscountAmount, + totalDiscountAmount, + + taxableAmount, + + ivaAmount, + recAmount, + retentionAmount, + + taxesAmount, + totalAmount, + } as const; + } + + public getSubtotalAmount(): ItemAmount { + return this.calculateAllAmounts().subtotalAmount; + } + + public getItemDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().itemDiscountAmount; + } + + public getGlobalDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().globalDiscountAmount; + } + + public getTotalDiscountAmount(): ItemAmount { + return this.calculateAllAmounts().totalDiscountAmount; + } + + public getTaxableAmount(): ItemAmount { + return this.calculateAllAmounts().taxableAmount; + } + + public getTaxesAmount(): ItemAmount { + return this.calculateAllAmounts().taxesAmount; + } + + public getTotalAmount(): ItemAmount { + return this.calculateAllAmounts().totalAmount; + } + + /** + * @summary Agrupa bases e importes por código fiscal. + * @remarks + * Este método se usa para poblar la tabla `customer_invoice_taxes`. + */ + public groupTaxesByCode() { const map = new Map(); for (const item of this.getAll()) { - const taxable = item.getTaxableAmount(); - const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts(); + const amounts = item.calculateAllAmounts(); + const taxable = amounts.taxableAmount; + const { ivaAmount, recAmount, retentionAmount } = amounts; + + /* ----------------------------- IVA ----------------------------- */ item.taxes.iva.match( (iva) => { - const prev = map.get(iva.code) ?? { + const key = iva.code; + const prev = map.get(key) ?? { + tax: iva, taxable: ItemAmount.zero(taxable.currencyCode), total: ItemAmount.zero(taxable.currencyCode), }; - map.set(iva.code, { + + map.set(key, { tax: iva, taxable: prev.taxable.add(taxable), total: prev.total.add(ivaAmount), @@ -145,13 +218,17 @@ export class CustomerInvoiceItems extends Collection { } ); + /* ----------------------------- REC ----------------------------- */ item.taxes.rec.match( (rec) => { - const prev = map.get(rec.code) ?? { + const key = rec.code; + const prev = map.get(key) ?? { + tax: rec, taxable: ItemAmount.zero(taxable.currencyCode), total: ItemAmount.zero(taxable.currencyCode), }; - map.set(rec.code, { + + map.set(key, { tax: rec, taxable: prev.taxable.add(taxable), total: prev.total.add(recAmount), @@ -162,13 +239,17 @@ export class CustomerInvoiceItems extends Collection { } ); + /* -------------------------- RETENCIÓN -------------------------- */ item.taxes.retention.match( (retention) => { - const prev = map.get(retention.code) ?? { + const key = retention.code; + const prev = map.get(key) ?? { + tax: retention, taxable: ItemAmount.zero(taxable.currencyCode), total: ItemAmount.zero(taxable.currencyCode), }; - map.set(retention.code, { + + map.set(key, { tax: retention, taxable: prev.taxable.add(taxable), total: prev.total.add(retentionAmount), diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts index e8af2461..ed47f4a2 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts @@ -1,19 +1,15 @@ -import { type Tax, Taxes } from "@erp/core/api"; +import { Collection } from "@repo/rdx-utils"; -import { InvoiceAmount } from "../../value-objects"; +import { InvoiceAmount, type InvoiceTaxGroup } from "../../value-objects"; -export type InvoiceTaxTotal = { - tax: Tax; - taxableAmount: InvoiceAmount; - taxesAmount: InvoiceAmount; -}; +export type InvoiceTaxTotal = {}; -export class InvoiceTaxes extends Taxes { - constructor(items: Tax[] = [], totalItems: number | null = null) { +export class InvoiceTaxes extends Collection { + constructor(items: InvoiceTaxGroup[] = [], totalItems: number | null = null) { super(items, totalItems); } - public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { + public getIVAAmount(): InvoiceAmount { return this.getAll().reduce( (total, tax) => total.add(taxableAmount.percentage(tax.percentage)), InvoiceAmount.zero(taxableAmount.currencyCode) diff --git a/modules/customer-invoices/src/api/domain/value-objects/index.ts b/modules/customer-invoices/src/api/domain/value-objects/index.ts index 1d921feb..8e2b70af 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/index.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/index.ts @@ -5,6 +5,7 @@ export * from "./customer-invoice-serie"; export * from "./customer-invoice-status"; export * from "./invoice-amount"; export * from "./invoice-recipient"; +export * from "./invoice-tax-group"; export * from "./item-amount"; export * from "./item-discount"; export * from "./item-quantity"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts new file mode 100644 index 00000000..4f0db471 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts @@ -0,0 +1,142 @@ +import type { Tax } from "@erp/core/api"; +import { ValueObject } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import { InvoiceAmount } from "./invoice-amount"; +import type { ItemTaxGroup } from "./item-tax-group"; + +export interface InvoiceTaxGroupProps { + taxableAmount: InvoiceAmount; + iva: Tax; + rec: Maybe; // si existe + retention: Maybe; // si existe +} + +export class InvoiceTaxGroup extends ValueObject { + static create(props: InvoiceTaxGroupProps) { + return Result.ok(new InvoiceTaxGroup(props)); + } + + /** + * Crea un grupo vacío a partir de un ItemTaxGroup (línea) + */ + static fromItem(lineTaxes: ItemTaxGroup, taxableAmount: InvoiceAmount): InvoiceTaxGroup { + const iva = lineTaxes.iva.unwrap(); // iva siempre obligatorio + const rec = lineTaxes.rec; + const retention = lineTaxes.retention; + + return new InvoiceTaxGroup({ + iva, + rec, + retention, + taxableAmount, + }); + } + + calculateAmounts() { + const taxableAmount = this.props.taxableAmount; + const ivaAmount = taxableAmount.percentage(this.props.iva.percentage); + + const recAmount = this.props.rec.match( + (rec) => taxableAmount.percentage(rec.percentage), + () => InvoiceAmount.zero(taxableAmount.currencyCode) + ); + + const retentionAmount = this.props.retention.match( + (retention) => taxableAmount.percentage(retention.percentage).multiply(-1), + () => InvoiceAmount.zero(taxableAmount.currencyCode) + ); + + const totalAmount = ivaAmount.add(recAmount).add(retentionAmount); + + return { ivaAmount, recAmount, retentionAmount, totalAmount }; + } + + get iva(): Tax { + return this.props.iva; + } + + get rec(): Maybe { + return this.props.rec; + } + + get retention(): Maybe { + return this.props.retention; + } + + get taxableAmount(): InvoiceAmount { + return this.props.taxableAmount; + } + + /** + * Clave única del grupo: iva|rec|ret + */ + public getKey(): string { + const iva = this.props.iva.code; + + const rec = this.props.rec.match( + (t) => t.code, + () => "" + ); + + const retention = this.props.retention.match( + (t) => t.code, + () => "" + ); + + return `${iva}|${rec}|${retention}`; + } + + /** + * Suma una base imponible a este grupo. + * + * Devuelve un nuevo InvoiceTaxGroup (inmutabilidad). + */ + public addTaxable(amount: InvoiceAmount): InvoiceTaxGroup { + return new InvoiceTaxGroup({ + ...this.props, + taxableAmount: this.props.taxableAmount.add(amount), + }); + } + + /** + * Devuelve únicamente los códigos existentes: ["iva_21", "rec_5_2"] + */ + public getCodesArray(): string[] { + const codes: string[] = []; + + // IVA + codes.push(this.props.iva.code); + + this.props.rec.match( + (t) => codes.push(t.code), + () => { + // + } + ); + + this.props.retention.match( + (t) => codes.push(t.code), + () => { + // + } + ); + + return codes; + } + + /** + * Devuelve una cadena tipo: "iva_21, rec_5_2" + */ + public getCodesToString(): string { + return this.getCodesArray().join(", "); + } + + getProps() { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts index 388997d1..0cddc7ea 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts @@ -105,6 +105,14 @@ export class CustomerInvoiceItemDomainMapper errors ); + const globalDiscountPercentage = extractOrPushError( + ItemDiscount.create({ + value: source.global_discount_percentage_value, + }), + `items[${index}].global_discount_percentage`, + errors + ); + const iva = extractOrPushError( maybeFromNullableVO(source.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), `items[${index}].iva_code`, @@ -133,7 +141,8 @@ export class CustomerInvoiceItemDomainMapper description, quantity, unitAmount, - discountPercentage, + itemDiscountPercentage: discountPercentage, + globalDiscountPercentage, taxes: ItemTaxGroup.create({ iva: iva!, @@ -171,7 +180,8 @@ export class CustomerInvoiceItemDomainMapper description: attributes.description!, quantity: attributes.quantity!, unitAmount: attributes.unitAmount!, - discountPercentage: attributes.discountPercentage!, + itemDiscountPercentage: attributes.itemDiscountPercentage!, + globalDiscountPercentage: attributes.globalDiscountPercentage!, taxes: attributes.taxes!, }, attributes.itemId @@ -198,7 +208,8 @@ export class CustomerInvoiceItemDomainMapper errors: ValidationErrorDetail[]; }; - const allAmounts = source.getAllAmounts(); + const allAmounts = source.calculateAllAmounts(); + const taxesAmounts = source.taxes.calculateAmounts(allAmounts.taxableAmount); return Result.ok({ item_id: source.id.toPrimitive(), @@ -218,29 +229,74 @@ export class CustomerInvoiceItemDomainMapper subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, + // discount_percentage_value: toNullable( - source.discountPercentage, + source.itemDiscountPercentage, (v) => v.toPrimitive().value ), discount_percentage_scale: - toNullable(source.discountPercentage, (v) => v.toPrimitive().scale) ?? + toNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? ItemDiscount.DEFAULT_SCALE, - discount_amount_value: allAmounts.discountAmount.value, - discount_amount_scale: allAmounts.discountAmount.scale, + discount_amount_value: allAmounts.itemDiscountAmount.value, + discount_amount_scale: allAmounts.itemDiscountAmount.scale, + // + global_discount_percentage_value: toNullable( + source.globalDiscountPercentage, + (v) => v.toPrimitive().value + ), + + global_discount_percentage_scale: + toNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, + + global_discount_amount_value: allAmounts.globalDiscountAmount.value, + global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, + + // + total_discount_amount_value: allAmounts.totalDiscountAmount.value, + total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, + + // taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_scale: allAmounts.taxableAmount.scale, + // IVA + iva_code: toNullable(source.taxes.iva, (v) => v.code), + + iva_percentage_value: toNullable(source.taxes.iva, (v) => v.percentage.value), + iva_percentage_scale: toNullable(source.taxes.iva, (v) => v.percentage.scale) ?? 2, + + iva_amount_value: taxesAmounts.ivaAmount.value, + iva_amount_scale: taxesAmounts.ivaAmount.scale, + + // REC + rec_code: toNullable(source.taxes.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.taxes.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.taxes.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: taxesAmounts.recAmount.value, + rec_amount_scale: taxesAmounts.recAmount.scale, + + // RET + retention_code: toNullable(source.taxes.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.taxes.retention, (v) => v.percentage.value), + retention_percentage_scale: + toNullable(source.taxes.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: taxesAmounts.retentionAmount.value, + retention_amount_scale: taxesAmounts.retentionAmount.scale, + + // taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_scale: allAmounts.taxesAmount.scale, + // total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, - - iva_code: toNullable(source.taxes.iva, (t) => t.code), - rec_code: toNullable(source.taxes.rec, (t) => t.code), - retention_code: toNullable(source.taxes.retention, (t) => t.code), }); } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts new file mode 100644 index 00000000..f83707b6 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-taxes.mapper.ts @@ -0,0 +1,150 @@ +import type { JsonTaxCatalogProvider } from "@erp/core"; +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { UniqueID, type ValidationErrorDetail, toNullable } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { CustomerInvoice, InvoiceTaxGroup } from "../../../domain"; +import type { + CustomerInvoiceTaxCreationAttributes, + CustomerInvoiceTaxModel, +} from "../../sequelize"; + +/** + * Mapper para customer_invoice_taxes + * + * Domina estructuras: + * { + * tax: Tax + * taxableAmount: ItemAmount + * taxesAmount: ItemAmount + * } + * + * Cada fila = un impuesto agregado en toda la factura. + */ +export class CustomerInvoiceTaxesDomainMapper extends SequelizeDomainMapper< + CustomerInvoiceTaxModel, + CustomerInvoiceTaxCreationAttributes, + InvoiceTaxGroup +> { + private _taxCatalog: JsonTaxCatalogProvider; + + constructor(params: MapperParamsType) { + super(); + const { taxCatalog } = params as { + taxCatalog: JsonTaxCatalogProvider; + }; + + if (!taxCatalog) { + throw new Error('taxCatalog not defined ("TaxesMapper")'); + } + + this._taxCatalog = taxCatalog; + } + + public mapToDomain( + source: CustomerInvoiceTaxModel, + params?: MapperParamsType + ): Result { + /*const { attributes, errors, index } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const currency_code = attributes.currencyCode!.code; + + const iva = extractOrPushError( + maybeFromNullableVO(source.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `taxes[${index}].iva_code`, + errors + ); + + const rec = extractOrPushError( + maybeFromNullableVO(source.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${index}].rec_code`, + errors + ); + + const retention = extractOrPushError( + maybeFromNullableVO(source.retention_code, (code) => + Tax.createFromCode(code, this._taxCatalog) + ), + `items[${index}].retention_code`, + errors + ); + + // Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice taxes mapping failed [mapToDomain]", errors) + ); + } + + const createResult = InvoiceTaxGroup.create({ + iva + }) + + return Result.ok();*/ + throw new Error("Se calcula a partir de las líneas de detalle"); + } + + public mapToPersistence( + source: InvoiceTaxGroup, + params?: MapperParamsType + ): Result { + const { errors, parent } = params as { + parent: CustomerInvoice; + errors: ValidationErrorDetail[]; + }; + + try { + const { ivaAmount, recAmount, retentionAmount } = source.calculateAmounts(); + + const totalTaxes = ivaAmount.add(recAmount).add(retentionAmount); + + const dto: CustomerInvoiceTaxCreationAttributes = { + tax_id: UniqueID.generateNewID().toPrimitive(), + invoice_id: parent.id.toPrimitive(), + + // TAXABLE AMOUNT + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + // IVA + iva_code: source.iva.code, + + iva_percentage_value: source.iva.value, + iva_percentage_scale: source.iva.scale, + + iva_amount_value: ivaAmount.value, + iva_amount_scale: ivaAmount.scale, + + // REC + rec_code: toNullable(source.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: recAmount.value, + rec_amount_scale: recAmount.scale, + + // RET + retention_code: toNullable(source.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.retention, (v) => v.percentage.value), + retention_percentage_scale: toNullable(source.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: retentionAmount.value, + retention_amount_scale: retentionAmount.scale, + + // TOTAL + taxes_amount_value: totalTaxes.value, + taxes_amount_scale: totalTaxes.scale, + }; + + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts index 46e2f0f6..3747705a 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts @@ -16,7 +16,7 @@ import { maybeFromNullableVO, toNullable, } from "@repo/rdx-ddd"; -import { Collection, Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; +import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { CustomerInvoice, @@ -30,8 +30,8 @@ import { import type { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceItemDomainMapper } from "./customer-invoice-item.mapper"; +import { CustomerInvoiceTaxesDomainMapper } from "./customer-invoice-taxes.mapper"; import { InvoiceRecipientDomainMapper } from "./invoice-recipient.mapper"; -import { TaxesDomainMapper } from "./invoice-taxes.mapper"; import { CustomerInvoiceVerifactuDomainMapper } from "./invoice-verifactu.mapper"; export interface ICustomerInvoiceDomainMapper @@ -51,7 +51,7 @@ export class CustomerInvoiceDomainMapper { private _itemsMapper: CustomerInvoiceItemDomainMapper; private _recipientMapper: InvoiceRecipientDomainMapper; - private _taxesMapper: TaxesDomainMapper; + private _taxesMapper: CustomerInvoiceTaxesDomainMapper; private _verifactuMapper: CustomerInvoiceVerifactuDomainMapper; constructor(params: MapperParamsType) { @@ -59,7 +59,7 @@ export class CustomerInvoiceDomainMapper this._itemsMapper = new CustomerInvoiceItemDomainMapper(params); // Instanciar el mapper de items this._recipientMapper = new InvoiceRecipientDomainMapper(); - this._taxesMapper = new TaxesDomainMapper(params); + this._taxesMapper = new CustomerInvoiceTaxesDomainMapper(params); this._verifactuMapper = new CustomerInvoiceVerifactuDomainMapper(); } @@ -250,6 +250,7 @@ export class CustomerInvoiceDomainMapper const items = CustomerInvoiceItems.create({ languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, + globalDiscountPercentage: attributes.discountPercentage!, items: itemsResults.data.getAll(), }); @@ -319,7 +320,7 @@ export class CustomerInvoiceDomainMapper } // 2) Taxes - const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), { + const taxesResult = this._taxesMapper.mapToPersistenceArray(source.getTaxes(), { errors, parent: source, ...params, @@ -357,7 +358,7 @@ export class CustomerInvoiceDomainMapper const taxes = taxesResult.data; const verifactu = verifactuResult.data; - const allAmounts = source.getAllAmounts(); // Da los totales ya calculados + const allAmounts = source.calculateAllAmounts(); // Da los totales ya calculados const invoiceValues: Partial = { // Identificación @@ -386,8 +387,8 @@ export class CustomerInvoiceDomainMapper discount_percentage_value: source.discountPercentage.toPrimitive().value, discount_percentage_scale: source.discountPercentage.toPrimitive().scale, - discount_amount_value: allAmounts.headerDiscountAmount.value, - discount_amount_scale: allAmounts.headerDiscountAmount.scale, + discount_amount_value: allAmounts.globalDiscountAmount.value, + discount_amount_scale: allAmounts.globalDiscountAmount.scale, taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_scale: allAmounts.taxableAmount.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts deleted file mode 100644 index 8f6c4e69..00000000 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { JsonTaxCatalogProvider } from "@erp/core"; -import { type MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; -import { UniqueID, type ValidationErrorDetail } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import { type CustomerInvoice, type CustomerInvoiceItemProps, ItemAmount } from "../../../domain"; -import type { - CustomerInvoiceTaxCreationAttributes, - CustomerInvoiceTaxModel, -} from "../../sequelize"; - -/** - * Mapper para customer_invoice_taxes - * - * Domina estructuras: - * { - * tax: Tax - * taxableAmount: ItemAmount - * taxesAmount: ItemAmount - * } - * - * Cada fila = un impuesto agregado en toda la factura. - */ -export class TaxesDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceTaxModel, - CustomerInvoiceTaxCreationAttributes, - { taxableAmount: ItemAmount; tax: Tax; taxesAmount: ItemAmount } -> { - private _taxCatalog: JsonTaxCatalogProvider; - - constructor(params: MapperParamsType) { - super(); - const { taxCatalog } = params as { - taxCatalog: JsonTaxCatalogProvider; - }; - - if (!taxCatalog) { - throw new Error('taxCatalog not defined ("TaxesMapper")'); - } - - this._taxCatalog = taxCatalog; - } - - public mapToDomain( - source: CustomerInvoiceTaxModel, - params?: MapperParamsType - ): Result< - { - taxableAmount: ItemAmount; - tax: Tax; - taxesAmount: ItemAmount; - }, - Error - > { - const { attributes } = params as { - attributes: Partial; - }; - - const currency_code = attributes.currencyCode!.code; - - return Result.ok({ - taxableAmount: ItemAmount.create({ - value: source.taxable_amount_value, - currency_code, - }).data, - tax: Tax.createFromCode(source.tax_code, this._taxCatalog).data, - taxesAmount: ItemAmount.create({ - value: source.taxes_amount_value, - currency_code, - }).data, - }); - } - - public mapToPersistence( - source: { - taxableAmount: ItemAmount; - tax: Tax; - taxesAmount: ItemAmount; - }, - params?: MapperParamsType - ): Result { - const { errors, parent } = params as { - parent: CustomerInvoice; - errors: ValidationErrorDetail[]; - }; - - return Result.ok({ - tax_id: UniqueID.generateNewID().toPrimitive(), - invoice_id: parent.id.toPrimitive(), - - tax_code: source.tax.code, - - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, - }); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts index d00e4179..624885c0 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts @@ -22,7 +22,6 @@ import { CustomerInvoiceStatus, InvoiceAmount, type InvoiceRecipient, - ItemAmount, type VerifactuRecord, } from "../../../domain"; import type { CustomerInvoiceModel } from "../../sequelize"; @@ -51,12 +50,6 @@ export type CustomerInvoiceListDTO = { languageCode: LanguageCode; currencyCode: CurrencyCode; - taxes: { - tax_code: string; - taxable_amount: InvoiceAmount; - taxes_amount: InvoiceAmount; - }[]; - discountPercentage: Percentage; subtotalAmount: InvoiceAmount; @@ -107,25 +100,6 @@ export class CustomerInvoiceListMapper }); } - // 3) Taxes - const taxes = raw.taxes.map((tax) => { - const taxableAmount = ItemAmount.create({ - value: tax.taxable_amount_value || 0, - currency_code: attributes.currencyCode!.code, - }).data; - - const taxesAmount = ItemAmount.create({ - value: tax.taxes_amount_value || 0, - currency_code: attributes.currencyCode!.code, - }).data; - - return { - tax_code: tax.tax_code, - taxable_amount: taxableAmount, - taxes_amount: taxesAmount, - }; - }); - // 4) Verifactu record let verifactu: Maybe = Maybe.none(); if (raw.verifactu) { @@ -167,8 +141,6 @@ export class CustomerInvoiceListMapper languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, - taxes, - discountPercentage: attributes.discountPercentage!, subtotalAmount: attributes.subtotalAmount!, discountAmount: attributes.discountAmount!, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts index 8648ab57..87a97f7a 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts @@ -32,7 +32,7 @@ export class CustomerInvoiceItemModel extends Model< declare unit_amount_value: CreationOptional; declare unit_amount_scale: number; - // Subtotal + // Subtotal (cantidad * importe unitario) declare subtotal_amount_value: CreationOptional; declare subtotal_amount_scale: number; @@ -44,12 +44,22 @@ export class CustomerInvoiceItemModel extends Model< declare discount_amount_value: CreationOptional; declare discount_amount_scale: number; - // Taxable amount (base imponible) + // Porcentaje de descuento global proporcional a esta línea. + declare global_discount_percentage_value: CreationOptional; + declare global_discount_percentage_scale: number; + + // Importe del descuento global para esta línea + declare global_discount_amount_value: CreationOptional; + declare global_discount_amount_scale: number; + + // Suma de los dos descuentos: el de la linea + el global + declare total_discount_amount_value: CreationOptional; + declare total_discount_amount_scale: number; + + // Taxable amount (base imponible tras los dos descuentos) declare taxable_amount_value: CreationOptional; declare taxable_amount_scale: number; - // Código de impuestos - // IVA percentage declare iva_code: CreationOptional; @@ -200,6 +210,42 @@ export default (database: Sequelize) => { defaultValue: 4, }, + global_discount_percentage_value: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + global_discount_percentage_scale: { + type: new DataTypes.SMALLINT(), + allowNull: false, + defaultValue: 2, + }, + + global_discount_amount_value: { + type: new DataTypes.BIGINT(), + allowNull: true, + defaultValue: null, + }, + + global_discount_amount_scale: { + type: new DataTypes.SMALLINT(), + allowNull: false, + defaultValue: 4, + }, + + total_discount_amount_value: { + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes + allowNull: true, + defaultValue: null, + }, + + total_discount_amount_scale: { + type: new DataTypes.SMALLINT(), + allowNull: false, + defaultValue: 4, + }, + taxable_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: true, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts index 3c9ddc5e..94ecd30f 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-tax.model.ts @@ -1,4 +1,5 @@ import { + type CreationOptional, DataTypes, type InferAttributes, type InferCreationAttributes, @@ -21,14 +22,45 @@ export class CustomerInvoiceTaxModel extends Model< declare tax_id: string; declare invoice_id: string; - declare tax_code: string; //"iva_21" - - // Taxable amount (base imponible) // 100,00 € - declare taxable_amount_value: number; + // Taxable amount (base imponible) + declare taxable_amount_value: CreationOptional; declare taxable_amount_scale: number; - // Total tax amount / taxes total // 21,00 € - declare taxes_amount_value: number; + // Código de impuestos + + // IVA percentage + declare iva_code: CreationOptional; + + declare iva_percentage_value: CreationOptional; + declare iva_percentage_scale: number; + + // IVA amount + + declare iva_amount_value: CreationOptional; + declare iva_amount_scale: number; + + // Recargo de equivalencia percentage + declare rec_code: CreationOptional; + + declare rec_percentage_value: CreationOptional; + declare rec_percentage_scale: number; + + // Recargo de equivalencia amount + declare rec_amount_value: CreationOptional; + declare rec_amount_scale: number; + + // Retention percentage + declare retention_code: CreationOptional; + + declare retention_percentage_value: CreationOptional; + declare retention_percentage_scale: number; + + // Retention amount + declare retention_amount_value: CreationOptional; + declare retention_amount_scale: number; + + // Total taxes amount / taxes total + declare taxes_amount_value: CreationOptional; declare taxes_amount_scale: number; // Relaciones @@ -74,33 +106,115 @@ export default (database: Sequelize) => { allowNull: false, }, - tax_code: { - type: new DataTypes.STRING(40), // Sugerido por IA - allowNull: false, - }, - taxable_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, + allowNull: true, + defaultValue: null, }, taxable_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, + defaultValue: 4, + }, + + // IVA % + + iva_code: { + type: DataTypes.STRING(40), + allowNull: true, + defaultValue: null, + }, + iva_percentage_value: { + type: DataTypes.SMALLINT, + allowNull: true, + defaultValue: null, + }, + iva_percentage_scale: { + type: DataTypes.SMALLINT, + allowNull: false, defaultValue: 2, }, + iva_amount_value: { + type: DataTypes.BIGINT, + allowNull: true, + defaultValue: null, + }, + iva_amount_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 4, + }, + + // REC % + rec_code: { + type: DataTypes.STRING(40), + allowNull: true, + defaultValue: null, + }, + + rec_percentage_value: { + type: DataTypes.SMALLINT, + allowNull: true, + defaultValue: null, + }, + rec_percentage_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 2, + }, + + rec_amount_value: { + type: DataTypes.BIGINT, + allowNull: true, + defaultValue: null, + }, + rec_amount_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 4, + }, + + // Retención % + retention_code: { + type: DataTypes.STRING(40), + allowNull: true, + defaultValue: null, + }, + + retention_percentage_value: { + type: DataTypes.SMALLINT, + allowNull: true, + defaultValue: null, + }, + retention_percentage_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 2, + }, + + 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, - defaultValue: 0, + allowNull: true, + defaultValue: null, }, taxes_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, - defaultValue: 2, + defaultValue: 4, }, }, { @@ -116,8 +230,8 @@ export default (database: Sequelize) => { fields: ["invoice_id"], }, { - name: "invoice_tax_code_unique", - fields: ["invoice_id", "tax_code"], + name: "invoice_iva_code_unique", + fields: ["invoice_id", "iva_code"], unique: true, // cada impuesto aparece como máximo una vez }, ], diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index b3c4dc4c..dcbd790e 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -35,8 +35,20 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ taxes: z.array( z.object({ - tax_code: z.string(), taxable_amount: MoneySchema, + + iva_code: z.string(), + iva_percentage: PercentageSchema, + iva_amount: MoneySchema, + + rec_code: z.string(), + rec_percentage: PercentageSchema, + rec_amount: MoneySchema, + + retention_code: z.string(), + retention_percentage: PercentageSchema, + retention_amount: MoneySchema, + taxes_amount: MoneySchema, }) ), @@ -53,6 +65,9 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ discount_percentage: PercentageSchema, discount_amount: MoneySchema, taxable_amount: MoneySchema, + iva_amount: MoneySchema, + rec_amount: MoneySchema, + retention_amount: MoneySchema, taxes_amount: MoneySchema, total_amount: MoneySchema, @@ -68,16 +83,34 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ is_valued: z.string(), position: z.string(), description: z.string(), + quantity: QuantitySchema, unit_amount: MoneySchema, - tax_codes: z.array(z.string()), - subtotal_amount: MoneySchema, + discount_percentage: PercentageSchema, discount_amount: MoneySchema, + + global_discount_percentage: PercentageSchema, + global_discount_amount: MoneySchema, + taxable_amount: MoneySchema, + + iva_code: z.string(), + iva_percentage: PercentageSchema, + iva_amount: MoneySchema, + + rec_code: z.string(), + rec_percentage: PercentageSchema, + rec_amount: MoneySchema, + + retention_code: z.string(), + retention_percentage: PercentageSchema, + retention_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, }) ), diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts index 2f896989..23c4f5e9 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/list-issued-invoices.response.dto.ts @@ -38,14 +38,6 @@ export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema( country: z.string(), }), - taxes: z.array( - z.object({ - tax_code: z.string(), - taxable_amount: MoneySchema, - taxes_amount: MoneySchema, - }) - ), - subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, discount_amount: MoneySchema, diff --git a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts index 45b6a029..44affb66 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts @@ -35,8 +35,20 @@ export const GetProformaByIdResponseSchema = z.object({ taxes: z.array( z.object({ - tax_code: z.string(), taxable_amount: MoneySchema, + + iva_code: z.string(), + iva_percentage: PercentageSchema, + iva_amount: MoneySchema, + + rec_code: z.string(), + rec_percentage: PercentageSchema, + rec_amount: MoneySchema, + + retention_code: z.string(), + retention_percentage: PercentageSchema, + retention_amount: MoneySchema, + taxes_amount: MoneySchema, }) ), @@ -53,6 +65,9 @@ export const GetProformaByIdResponseSchema = z.object({ discount_percentage: PercentageSchema, discount_amount: MoneySchema, taxable_amount: MoneySchema, + iva_amount: MoneySchema, + rec_amount: MoneySchema, + retention_amount: MoneySchema, taxes_amount: MoneySchema, total_amount: MoneySchema, @@ -62,16 +77,34 @@ export const GetProformaByIdResponseSchema = z.object({ is_valued: z.string(), position: z.string(), description: z.string(), + quantity: QuantitySchema, unit_amount: MoneySchema, - tax_codes: z.array(z.string()), - subtotal_amount: MoneySchema, + discount_percentage: PercentageSchema, discount_amount: MoneySchema, + + global_discount_percentage: PercentageSchema, + global_discount_amount: MoneySchema, + taxable_amount: MoneySchema, + + iva_code: z.string(), + iva_percentage: PercentageSchema, + iva_amount: MoneySchema, + + rec_code: z.string(), + rec_percentage: PercentageSchema, + rec_amount: MoneySchema, + + retention_code: z.string(), + retention_percentage: PercentageSchema, + retention_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, }) ), diff --git a/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts index 0a6b68ad..28652083 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/list-proformas.response.dto.ts @@ -38,14 +38,6 @@ export const ListProformasResponseSchema = createPaginatedListSchema( country: z.string(), }), - taxes: z.array( - z.object({ - tax_code: z.string(), - taxable_amount: MoneySchema, - taxes_amount: MoneySchema, - }) - ), - subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, discount_amount: MoneySchema, diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index 4ab62530..314031bd 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -1,8 +1,9 @@ -import { enforceTenant, enforceUser, mockUser, RequestWithAuth } from "@erp/auth/api"; -import { ModuleParams, validateRequest } from "@erp/core/api"; -import { ILogger } from "@repo/rdx-logger"; -import { Application, NextFunction, Request, Response, Router } from "express"; -import { Sequelize } from "sequelize"; +import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; +import { type ModuleParams, validateRequest } from "@erp/core/api"; +import type { ILogger } from "@repo/rdx-logger"; +import { type Application, type NextFunction, type Request, type Response, Router } from "express"; +import type { Sequelize } from "sequelize"; + import { CreateCustomerRequestSchema, CustomerListRequestSchema, @@ -11,6 +12,7 @@ import { UpdateCustomerByIdRequestSchema, } from "../../../common/dto"; import { buildCustomerDependencies } from "../dependencies"; + import { CreateCustomerController, GetCustomerController, @@ -31,7 +33,7 @@ export const customersRouter = (params: ModuleParams) => { const router: Router = Router({ mergeParams: true }); // 🔐 Autenticación + Tenancy para TODO el router - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { router.use( (req: Request, res: Response, next: NextFunction) => mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas