diff --git a/modules/core/src/api/domain/value-objects/tax.ts b/modules/core/src/api/domain/value-objects/tax.ts index 3e0bbd47..10623b82 100644 --- a/modules/core/src/api/domain/value-objects/tax.ts +++ b/modules/core/src/api/domain/value-objects/tax.ts @@ -1,4 +1,4 @@ -import { TaxCatalogProvider } from "@erp/core"; +import type { TaxCatalogProvider } from "@erp/core"; import { Percentage, ValueObject } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; 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 f6cd5f08..4ff1084b 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 @@ -42,7 +42,7 @@ export class IssuedInvoiceItemsFullPresenter extends Presenter { discount_amount: allAmounts.discountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), - tax_codes: invoiceItem.taxes.getCodesToString().split(","), + tax_codes: invoiceItem.taxes.getCodesArray(), taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), 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 a135802d..58110282 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 @@ -40,7 +40,7 @@ export class ProformaItemsFullPresenter extends Presenter { discount_amount: allAmounts.discountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), - tax_codes: proformaItem.taxes.getCodesToString().split(","), + tax_codes: proformaItem.taxes.getCodesArray(), taxes_amount: allAmounts.taxesAmount.toObjectString(), total_amount: allAmounts.totalAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts index 53f62427..dab85028 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/issued-invoices/issued-invoice.list.presenter.ts @@ -39,7 +39,11 @@ export class IssuedInvoiceListPresenter extends Presenter { language_code: invoice.languageCode.code, currency_code: invoice.currencyCode.code, - taxes: invoice.taxes, + taxes: invoice.taxes.map((t) => ({ + 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(), 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 c946c225..e36872f2 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,7 +30,11 @@ export class ProformaListPresenter extends Presenter { language_code: proforma.languageCode.code, currency_code: proforma.currencyCode.code, - taxes: proforma.taxes, + 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(), 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 946f5d6a..49d53e9d 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 @@ -18,9 +18,14 @@ export class IssuedInvoiceTaxesReportPresenter extends Presenter item.name, + () => taxItem.tax_code // fallback + ); + return { tax_code: taxItem.tax_code, - tax_name: taxCatalogItem.unwrap().name, + tax_name: taxName, taxable_amount: MoneyDTOHelper.format(taxItem.taxable_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 d2ba0a6d..7f87816c 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 @@ -18,9 +18,14 @@ export class ProformaTaxesReportPresenter extends Presenter item.name, + () => taxItem.tax_code // fallback + ); + return { tax_code: taxItem.tax_code, - tax_name: taxCatalogItem.unwrap().name, + tax_name: taxName, taxable_amount: MoneyDTOHelper.format(taxItem.taxable_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/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts index fe99ab63..a4d13099 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.html.ts @@ -23,9 +23,6 @@ export class IssuedInvoiceReportHTMLPresenter extends TemplatePresenter { const invoiceDTO = dtoPresenter.toOutput(invoice); const prettyDTO = prePresenter.toOutput(invoiceDTO); - console.log(prettyDTO.verifactu); - - // Obtener y compilar la plantilla HTML const template = this.templateResolver.compileTemplate( "customer-invoices", diff --git a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts index 9ce6b19f..fd4b5963 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issued-invoices/report-issued-invoices/reporter/issued-invoice.report.pdf.ts @@ -23,8 +23,6 @@ export class IssuedInvoiceReportPDFPresenter extends Presenter< format: "HTML", }) as IssuedInvoiceReportHTMLPresenter; - console.log(invoice); - const htmlData = htmlPresenter.toOutput(invoice, params); // Generar el PDF con Puppeteer 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 62f32696..c71be482 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -236,6 +236,26 @@ export class CustomerInvoice return this.paymentMethod.isSome(); } + /* CALCULOS INTERNOS */ + + private _getSubtotalAmount(): InvoiceAmount { + const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2); + + return InvoiceAmount.create({ + value: itemsSubtotal.value, + currency_code: this.currencyCode.code, + }).data; + } + + private _getItemsDiscountAmount(): InvoiceAmount { + const itemsDiscountAmount = this.items.getDiscountAmount().convertScale(2); + + return InvoiceAmount.create({ + value: itemsDiscountAmount.value, + currency_code: this.currencyCode.code, + }).data; + } + private _getHeaderDiscountAmount( subtotalAmount: InvoiceAmount, itemsDiscountAmount: InvoiceAmount @@ -251,79 +271,43 @@ export class CustomerInvoice return subtotalAmount.subtract(itemsDiscountAmount).subtract(headerDiscountAmount); } - private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { - let amount = InvoiceAmount.zero(this.currencyCode.code); - - const itemTaxes = this.items.getTaxesAmountByTaxes(); - - for (const taxItem of itemTaxes) { - amount = amount.add( - InvoiceAmount.create({ - value: taxItem.taxesAmount.convertScale(2).value, - currency_code: this.currencyCode.code, - }).data - ); - } - return amount; + // 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; } private _getTotalAmount(taxableAmount: InvoiceAmount, taxesAmount: InvoiceAmount): InvoiceAmount { return taxableAmount.add(taxesAmount); } - public _getSubtotalAmount(): InvoiceAmount { - const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2); - - return InvoiceAmount.create({ - value: itemsSubtotal.value, - currency_code: this.currencyCode.code, - }).data as InvoiceAmount; - } - - public _getItemsDiscountAmount(): InvoiceAmount { - const itemsDiscountAmount = this.items.getDiscountAmount().convertScale(2); - - return InvoiceAmount.create({ - value: itemsDiscountAmount.value, - currency_code: this.currencyCode.code, - }).data as InvoiceAmount; - } - - /*public getHeaderDiscountAmount(): InvoiceAmount { - return this._getHeaderDiscountAmount(this.getSubtotalAmount()); - } - - public getTaxableAmount(): InvoiceAmount { - return this._getTaxableAmount(this.getSubtotalAmount(), this.getHeaderDiscountAmount()); - } - - public getTaxesAmount(): InvoiceAmount { - return this._getTaxesAmount(this.getTaxableAmount()); - } - - public getTotalAmount(): InvoiceAmount { - const taxableAmount = this.getTaxableAmount(); - const taxesAmount = this._getTaxesAmount(taxableAmount); - - return this._getTotalAmount(taxableAmount, taxesAmount); - }*/ + /** Totales expuestos */ public getTaxes(): InvoiceTaxTotal[] { - const itemTaxes = this.items.getTaxesAmountByTaxes(); + const map = this.items.getAggregatedTaxesByCode(); + const currency = this.currencyCode.code; - return itemTaxes.map((item) => { - return { - tax: item.tax, + const result: InvoiceTaxTotal[] = []; + + for (const [tax_code, entry] of map.entries()) { + result.push({ + tax: entry.tax, taxableAmount: InvoiceAmount.create({ - value: item.taxableAmount.convertScale(2).value, - currency_code: this.currencyCode.code, + value: entry.taxable.convertScale(2).value, + currency_code: currency, }).data, taxesAmount: InvoiceAmount.create({ - value: item.taxesAmount.convertScale(2).value, - currency_code: this.currencyCode.code, + value: entry.total.convertScale(2).value, + currency_code: currency, }).data, - }; - }); + }); + } + + return result; } public getAllAmounts() { @@ -337,7 +321,7 @@ export class CustomerInvoice headerDiscountAmount ); // - const taxesAmount = this._getTaxesAmount(taxableAmount); + const taxesAmount = this._getTaxesAmount(); const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount); return { 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 c491b0b0..d01e0b60 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 @@ -1,10 +1,4 @@ -import { - type CurrencyCode, - DomainEntity, - type LanguageCode, - type Percentage, - type UniqueID, -} from "@repo/rdx-ddd"; +import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; import { @@ -13,7 +7,7 @@ import { ItemDiscount, ItemQuantity, } from "../../value-objects"; -import type { ItemTaxTotal, ItemTaxes } from "../item-taxes"; +import type { ItemTaxGroup } from "../../value-objects/item-tax-group"; export interface CustomerInvoiceItemProps { description: Maybe; @@ -22,7 +16,7 @@ export interface CustomerInvoiceItemProps { discountPercentage: Maybe; // % descuento - taxes: ItemTaxes; + taxes: ItemTaxGroup; languageCode: LanguageCode; currencyCode: CurrencyCode; @@ -38,7 +32,7 @@ export interface ICustomerInvoiceItem { discountPercentage: Maybe; // % descuento - taxes: ItemTaxes; + taxes: ItemTaxGroup; languageCode: LanguageCode; currencyCode: CurrencyCode; @@ -80,31 +74,25 @@ export class CustomerInvoiceItem return this._isValued; } - get description(): Maybe { + get description() { return this.props.description; } - - get quantity(): Maybe { + get quantity() { return this.props.quantity; } - - get unitAmount(): Maybe { + get unitAmount() { return this.props.unitAmount; } - - get discountPercentage(): Maybe { + get discountPercentage() { return this.props.discountPercentage; } - - get languageCode(): LanguageCode { + get languageCode() { return this.props.languageCode; } - - get currencyCode(): CurrencyCode { + get currencyCode() { return this.props.currencyCode; } - - get taxes(): ItemTaxes { + get taxes() { return this.props.taxes; } @@ -124,7 +112,7 @@ export class CustomerInvoiceItem */ private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount { const discount = this.discountPercentage.match( - (percentage) => percentage, + (discount) => discount, () => ItemDiscount.zero() ); return subtotalAmount.percentage(discount); @@ -141,6 +129,11 @@ export class CustomerInvoiceItem 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. @@ -148,7 +141,8 @@ export class CustomerInvoiceItem * @returns El importe de impuestos calculado. */ private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { - return this.props.taxes.getTaxesAmount(taxableAmount); + const { ivaAmount, recAmount, retentionAmount } = this._getIndividualTaxAmounts(taxableAmount); + return ivaAmount.add(recAmount).add(retentionAmount); // retención ya es negativa } /** @@ -169,17 +163,15 @@ export class CustomerInvoiceItem * Si la cantidad o el importe unitario no están definidos, se asumen valores cero. */ public getSubtotalAmount(): ItemAmount { - const curCode = this.currencyCode.code; - const quantity = this.quantity.match( + const qty = this.quantity.match( (quantity) => quantity, () => ItemQuantity.zero() ); - const unitAmount = this.unitAmount.match( + const unit = this.unitAmount.match( (unitAmount) => unitAmount, - () => ItemAmount.zero(curCode) + () => ItemAmount.zero(this.currencyCode.code) ); - - return unitAmount.multiply(quantity); + return unit.multiply(qty); } /** @@ -198,6 +190,11 @@ export class CustomerInvoiceItem return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount()); } + /* importes individuales: iva / rec / ret */ + public getIndividualTaxAmounts() { + return this._getIndividualTaxAmounts(this.getTaxableAmount()); + } + /** * @summary Calcula el importe total de impuestos aplicados al ítem. * @returns Un `ItemAmount` con el total de impuestos. @@ -217,14 +214,6 @@ export class CustomerInvoiceItem return this._getTotalAmount(taxableAmount, taxesAmount); } - /** - * @summary Obtiene los importes de impuestos agrupados por tipo de impuesto. - * @returns Una colección con la base imponible y el importe de impuestos por cada tipo. - */ - public getTaxesAmountByTaxes(): ItemTaxTotal[] { - return this.taxes.getTaxesAmountByTaxes(this.getTaxableAmount()); - } - /** * @summary Devuelve todos los importes calculados del ítem en un único objeto. * @returns Un objeto con las propiedades: 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 f0225fe5..ebe26340 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 @@ -3,7 +3,6 @@ import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount } from "../../value-objects"; -import { ItemTaxes } from "../item-taxes"; import type { CustomerInvoiceItem } from "./customer-invoice-item"; @@ -18,10 +17,9 @@ export class CustomerInvoiceItems extends Collection { private _currencyCode!: CurrencyCode; constructor(props: CustomerInvoiceItemsProps) { - const { items = [], languageCode, currencyCode } = props; - super(items); - this._languageCode = languageCode; - this._currencyCode = currencyCode; + super(props.items ?? []); + this._languageCode = props.languageCode; + this._currencyCode = props.currencyCode; } public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { @@ -105,65 +103,83 @@ export class CustomerInvoiceItems extends Collection { ); } - /** - * @summary Agrupa los importes imponibles e impuestos por tipo de impuesto. - * @returns Un array con objetos que contienen: - * - `tax`: El tipo de impuesto. - * - `taxableAmount`: El total de base imponible asociada a ese impuesto. - * - `taxesAmount`: El importe total de impuestos calculado. - * @remarks - * Los impuestos se agrupan por su `tax.code` (código fiscal). - * Si existen varios impuestos con el mismo código, sus importes se agregan. - * En caso de conflicto de datos en impuestos con mismo código, prevalece - * el primer `Tax` encontrado. - */ - public getTaxesAmountByTaxes(): Array<{ - tax: Tax; - taxableAmount: ItemAmount; - taxesAmount: ItemAmount; - }> { - const getTaxCode = (tax: Tax): string => tax.code; // clave estable para Map - const currencyCode = this._currencyCode.code; - - // Mapeamos por clave (tax code), pero también guardamos el Tax original - const resultMap = new Map< - string, - { tax: Tax; taxableAmount: ItemAmount; taxesAmount: ItemAmount } - >(); + /* totales de iva/rec/ret a nivel factura */ + public getAggregatedTaxesByType() { + let iva = ItemAmount.zero(this._currencyCode.code); + let rec = ItemAmount.zero(this._currencyCode.code); + let retention = ItemAmount.zero(this._currencyCode.code); for (const item of this.getAll()) { - for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) { - const key = getTaxCode(tax); - const current = resultMap.get(key) ?? { - tax, - taxableAmount: ItemAmount.zero(currencyCode), - taxesAmount: ItemAmount.zero(currencyCode), - }; + const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts(); - resultMap.set(key, { - tax: current.tax, - taxableAmount: current.taxableAmount.add(taxableAmount), - taxesAmount: current.taxesAmount.add(taxesAmount), - }); - } + iva = iva.add(ivaAmount); + rec = rec.add(recAmount); + retention = retention.add(retentionAmount); } - return Array.from(resultMap.values()); + return { iva, rec, retention }; } - /** - * @summary Obtiene la lista de impuestos únicos aplicados en todos los ítems. - * @returns Un objeto `ItemTaxes` que contiene todos los impuestos distintos. - * @remarks - * Los impuestos se deduplican por su código (`tax.code`). - * Si existen varios impuestos con el mismo código, el último encontrado - * sobrescribirá los anteriores sin generar error. - */ - public getTaxes(): ItemTaxes { - const taxes = this.getAll() - .flatMap((item) => item.taxes.getAll()) - .reduce((map, tax) => map.set(tax.code, tax), new Map()); + /* agrupación por código fiscal → usado para customer_invoice_taxes */ + public getAggregatedTaxesByCode() { + const map = new Map(); - return ItemTaxes.create([...taxes.values()]); + for (const item of this.getAll()) { + const taxable = item.getTaxableAmount(); + const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts(); + + item.taxes.iva.match( + (iva) => { + const prev = map.get(iva.code) ?? { + taxable: ItemAmount.zero(taxable.currencyCode), + total: ItemAmount.zero(taxable.currencyCode), + }; + map.set(iva.code, { + tax: iva, + taxable: prev.taxable.add(taxable), + total: prev.total.add(ivaAmount), + }); + }, + () => { + // + } + ); + + item.taxes.rec.match( + (rec) => { + const prev = map.get(rec.code) ?? { + taxable: ItemAmount.zero(taxable.currencyCode), + total: ItemAmount.zero(taxable.currencyCode), + }; + map.set(rec.code, { + tax: rec, + taxable: prev.taxable.add(taxable), + total: prev.total.add(recAmount), + }); + }, + () => { + // + } + ); + + item.taxes.retention.match( + (retention) => { + const prev = map.get(retention.code) ?? { + taxable: ItemAmount.zero(taxable.currencyCode), + total: ItemAmount.zero(taxable.currencyCode), + }; + map.set(retention.code, { + tax: retention, + taxable: prev.taxable.add(taxable), + total: prev.total.add(retentionAmount), + }); + }, + () => { + // + } + ); + } + + return map; } } diff --git a/modules/customer-invoices/src/api/domain/entities/index.ts b/modules/customer-invoices/src/api/domain/entities/index.ts index 1ab175ce..a6986742 100644 --- a/modules/customer-invoices/src/api/domain/entities/index.ts +++ b/modules/customer-invoices/src/api/domain/entities/index.ts @@ -1,5 +1,4 @@ export * from "./customer-invoice-items"; export * from "./invoice-payment-method"; export * from "./invoice-taxes"; -export * from "./item-taxes"; export * from "./verifactu-record"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts index 058ada84..9a07598d 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts @@ -1,7 +1,8 @@ -import { Tax } from "@erp/core/api"; -import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; +import type { Tax } from "@erp/core/api"; +import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { InvoiceAmount } from "../../value-objects/invoice-amount"; + +import type { InvoiceAmount } from "../../value-objects/invoice-amount"; export interface InvoiceTaxProps { tax: Tax; 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 d1ce77da..e8af2461 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,4 +1,5 @@ -import { Tax, Taxes } from "@erp/core/api"; +import { type Tax, Taxes } from "@erp/core/api"; + import { InvoiceAmount } from "../../value-objects"; export type InvoiceTaxTotal = { diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts deleted file mode 100644 index de5994ce..00000000 --- a/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./item-taxes"; diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts deleted file mode 100644 index d4c3acfe..00000000 --- a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { type Tax, Taxes } from "@erp/core/api"; - -import { ItemAmount } from "../../value-objects"; - -export type ItemTaxTotal = { - tax: Tax; - taxableAmount: ItemAmount; - taxesAmount: ItemAmount; -}; - -export class ItemTaxes extends Taxes { - constructor(items: Tax[] = [], totalItems: number | null = null) { - super(items, totalItems); - } - - public getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { - return this.getAll().reduce( - (total, tax) => total.add(taxableAmount.percentage(tax.percentage)), - ItemAmount.zero(taxableAmount.currencyCode) - ); - } - - public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount { - const currencyCode = taxableAmount.currencyCode; - - return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => { - return taxableAmount.percentage(itemTax.percentage).add(totalAmount); - }, ItemAmount.zero(currencyCode)); - } - - public getTaxesAmountByTaxes(taxableAmount: ItemAmount): ItemTaxTotal[] { - return this.getAll().map((taxItem) => ({ - taxableAmount, - tax: taxItem, - taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount), - })); - } - - public getCodesToString(): string { - return this.getAll() - .map((taxItem) => taxItem.code) - .join(", "); - } -} diff --git a/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts b/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts index 4808a802..873b668d 100644 --- a/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts +++ b/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts @@ -1,6 +1,7 @@ -import { UniqueID } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects"; +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects"; /** * Servicio de dominio que define cómo se genera el siguiente número de factura. diff --git a/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts b/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts index 0cd58f96..c2708b31 100644 --- a/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts +++ b/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts @@ -1,5 +1,6 @@ import { CompositeSpecification } from "@repo/rdx-ddd"; -import { CustomerInvoice } from "../aggregates"; + +import type { CustomerInvoice } from "../aggregates"; import { INVOICE_STATUS } from "../value-objects"; export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification { 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 5a0fe743..1d921feb 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/index.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/index.ts @@ -8,4 +8,5 @@ export * from "./invoice-recipient"; export * from "./item-amount"; export * from "./item-discount"; export * from "./item-quantity"; +export * from "./item-tax-group"; export * from "./verifactu-status"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts index e4cbcae1..e9e02b8a 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts @@ -1,4 +1,4 @@ -import { MoneyValue, MoneyValueProps, Percentage, Quantity } from "@repo/rdx-ddd"; +import { MoneyValue, type MoneyValueProps, type Percentage, type Quantity } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; type InvoiceAmountProps = Pick; diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts index 04d22c79..8d13e53c 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts @@ -1,4 +1,4 @@ -import { MoneyValue, MoneyValueProps, Percentage, Quantity } from "@repo/rdx-ddd"; +import { MoneyValue, type MoneyValueProps, type Percentage, type Quantity } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; type ItemAmountProps = Pick; diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts index a3f53686..78237aff 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts @@ -1,5 +1,5 @@ -import { Percentage, PercentageProps } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Percentage, type PercentageProps } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; type ItemDiscountProps = Pick; diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts b/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts index 27a064ac..797bb751 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts @@ -1,4 +1,4 @@ -import { Quantity, QuantityProps } from "@repo/rdx-ddd"; +import { Quantity, type QuantityProps } from "@repo/rdx-ddd"; type ItemQuantityProps = Pick; diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-tax-group.ts b/modules/customer-invoices/src/api/domain/value-objects/item-tax-group.ts new file mode 100644 index 00000000..752d1bcd --- /dev/null +++ b/modules/customer-invoices/src/api/domain/value-objects/item-tax-group.ts @@ -0,0 +1,87 @@ +import type { Tax } from "@erp/core/api"; +import { ValueObject } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import { ItemAmount } from "."; + +export interface ItemTaxGroupProps { + iva: Maybe; // si existe + rec: Maybe; // si existe + retention: Maybe; // si existe +} + +export class ItemTaxGroup extends ValueObject { + static create(props: ItemTaxGroupProps) { + return Result.ok(new ItemTaxGroup(props)); + } + + calculateAmounts(taxableAmount: ItemAmount) { + const ivaAmount = this.props.iva.match( + (iva) => taxableAmount.percentage(iva.percentage), + () => ItemAmount.zero(taxableAmount.currencyCode) + ); + + const recAmount = this.props.rec.match( + (rec) => taxableAmount.percentage(rec.percentage), + () => ItemAmount.zero(taxableAmount.currencyCode) + ); + + const retentionAmount = this.props.retention.match( + (retention) => taxableAmount.percentage(retention.percentage).multiply(-1), + () => ItemAmount.zero(taxableAmount.currencyCode) + ); + + return { ivaAmount, recAmount, retentionAmount }; + } + + get iva(): Maybe { + return this.props.iva; + } + + get rec(): Maybe { + return this.props.rec; + } + + get retention(): Maybe { + return this.props.retention; + } + + public getCodesArray(): string[] { + const codes: string[] = []; + + this.props.iva.match( + (iva) => codes.push(iva.code), + () => { + // + } + ); + + this.props.rec.match( + (rec) => codes.push(rec.code), + () => { + // + } + ); + + this.props.retention.match( + (retention) => codes.push(retention.code), + () => { + // + } + ); + + return codes; + } + + public getCodesToString(): string { + return this.getCodesArray().join(", "); + } + + getProps() { + return this.props; + } + + 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 b389daa2..388997d1 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 @@ -1,7 +1,9 @@ +import type { JsonTaxCatalogProvider } from "@erp/core"; import { type ISequelizeDomainMapper, type MapperParamsType, SequelizeDomainMapper, + Tax, } from "@erp/core/api"; import { UniqueID, @@ -22,15 +24,13 @@ import { ItemAmount, ItemDiscount, ItemQuantity, - ItemTaxes, + ItemTaxGroup, } from "../../../domain"; import type { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel, } from "../../sequelize"; -import { ItemTaxesDomainMapper } from "./item-taxes.mapper"; - export interface ICustomerInvoiceItemDomainMapper extends ISequelizeDomainMapper< CustomerInvoiceItemModel, @@ -46,11 +46,19 @@ export class CustomerInvoiceItemDomainMapper > implements ICustomerInvoiceItemDomainMapper { - private _taxesMapper: ItemTaxesDomainMapper; + private _taxCatalog!: JsonTaxCatalogProvider; constructor(params: MapperParamsType) { super(); - this._taxesMapper = new ItemTaxesDomainMapper(params); + const { taxCatalog } = params as { + taxCatalog: JsonTaxCatalogProvider; + }; + + if (!taxCatalog) { + throw new Error('taxCatalog not defined ("CustomerInvoiceItemDomainMapper")'); + } + + this._taxCatalog = taxCatalog; } private mapAttributesToDomain( @@ -97,6 +105,26 @@ export class CustomerInvoiceItemDomainMapper errors ); + const iva = extractOrPushError( + maybeFromNullableVO(source.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${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 + ); + return { itemId, @@ -106,6 +134,12 @@ export class CustomerInvoiceItemDomainMapper quantity, unitAmount, discountPercentage, + + taxes: ItemTaxGroup.create({ + iva: iva!, + rec: rec!, + retention: retention!, + }).data, }; } @@ -122,23 +156,6 @@ export class CustomerInvoiceItemDomainMapper // 1) Valores escalares (atributos generales) const attributes = this.mapAttributesToDomain(source, params); - // 2) Taxes (colección a nivel de item/línea) - const taxesResults = this._taxesMapper.mapToDomainCollection( - source.taxes, - source.taxes.length, - { - attributes, - ...params, - } - ); - - if (taxesResults.isFailure) { - errors.push({ - path: "taxes", - message: taxesResults.error.message, - }); - } - // Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( @@ -146,9 +163,7 @@ export class CustomerInvoiceItemDomainMapper ); } - const taxes = ItemTaxes.create(taxesResults.data.getAll()); - - // 3) Construcción del elemento de dominio + // 2) Construcción del elemento de dominio const createResult = CustomerInvoiceItem.create( { languageCode: attributes.languageCode!, @@ -157,7 +172,7 @@ export class CustomerInvoiceItemDomainMapper quantity: attributes.quantity!, unitAmount: attributes.unitAmount!, discountPercentage: attributes.discountPercentage!, - taxes, + taxes: attributes.taxes!, }, attributes.itemId ); @@ -183,18 +198,6 @@ export class CustomerInvoiceItemDomainMapper errors: ValidationErrorDetail[]; }; - const taxesResults = this._taxesMapper.mapToPersistenceArray(source.taxes, { - ...params, - parent: source, - }); - - if (taxesResults.isFailure) { - errors.push({ - path: "taxes", - message: taxesResults.error.message, - }); - } - const allAmounts = source.getAllAmounts(); return Result.ok({ @@ -235,7 +238,9 @@ export class CustomerInvoiceItemDomainMapper total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, - taxes: taxesResults.data, + 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.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts index 5b3db97d..46e2f0f6 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 @@ -220,14 +220,6 @@ export class CustomerInvoiceDomainMapper ...params, }); - /*if (recipientResult.isFailure) { - errors.push({ - path: "recipient", - - message: recipientResult.error.message, - }); - }*/ - // 3) Verifactu (snapshot en la factura o include) const verifactuResult = this._verifactuMapper.mapToDomain(source.verifactu, { errors, @@ -235,14 +227,6 @@ export class CustomerInvoiceDomainMapper ...params, }); - /*if (verifactuResult.isFailure) { - errors.push({ - path: "verifactu", - - message: verifactuResult.error.message, - }); - }*/ - // 4) Items (colección) const itemsResults = this._itemsMapper.mapToDomainCollection( source.items, @@ -254,16 +238,6 @@ export class CustomerInvoiceDomainMapper } ); - /*if (itemsResults.isFailure) { - errors.push({ - path: "items", - message: itemsResults.error.message, - }); - }*/ - - // Nota: los impuestos a nivel factura (tabla customer_invoice_taxes) se derivan de los items. - // El agregado expone un getter `taxes` (derivado). No se incluye en las props. - // 5) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( @@ -273,9 +247,6 @@ export class CustomerInvoiceDomainMapper // 6) Construcción del agregado (Dominio) - const verifactu = verifactuResult.data; - const recipient = recipientResult.data; - const items = CustomerInvoiceItems.create({ languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, @@ -294,7 +265,7 @@ export class CustomerInvoiceDomainMapper operationDate: attributes.operationDate!, customerId: attributes.customerId!, - recipient: recipient, + recipient: recipientResult.data, reference: attributes.reference!, description: attributes.description!, @@ -308,7 +279,7 @@ export class CustomerInvoiceDomainMapper paymentMethod: attributes.paymentMethod!, items, - verifactu, + verifactu: verifactuResult.data, }; const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId); 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 index 3d982627..8f6c4e69 100644 --- 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 @@ -1,11 +1,26 @@ -import { JsonTaxCatalogProvider } from "@erp/core"; -import { MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; -import { UniqueID, ValidationErrorDetail } from "@repo/rdx-ddd"; - +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 { CustomerInvoice, CustomerInvoiceItemProps, ItemAmount } from "../../../domain"; -import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize"; +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, @@ -41,15 +56,17 @@ export class TaxesDomainMapper extends SequelizeDomainMapper< attributes: Partial; }; + const currency_code = attributes.currencyCode!.code; + return Result.ok({ taxableAmount: ItemAmount.create({ value: source.taxable_amount_value, - currency_code: attributes.currencyCode!.code, + currency_code, }).data, tax: Tax.createFromCode(source.tax_code, this._taxCatalog).data, taxesAmount: ItemAmount.create({ value: source.taxes_amount_value, - currency_code: attributes.currencyCode!.code, + currency_code, }).data, }); } @@ -67,8 +84,6 @@ export class TaxesDomainMapper extends SequelizeDomainMapper< errors: ValidationErrorDetail[]; }; - source; - return Result.ok({ tax_id: UniqueID.generateNewID().toPrimitive(), invoice_id: parent.id.toPrimitive(), diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts deleted file mode 100644 index 7187fe58..00000000 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { JsonTaxCatalogProvider } from "@erp/core"; -import { - ISequelizeDomainMapper, - MapperParamsType, - SequelizeDomainMapper, - Tax, -} from "@erp/core/api"; - -import { - extractOrPushError, - UniqueID, - ValidationErrorCollection, - ValidationErrorDetail, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { CustomerInvoiceItem } from "../../../domain"; -import { - CustomerInvoiceItemTaxCreationAttributes, - CustomerInvoiceItemTaxModel, -} from "../../sequelize"; - -export interface IItemTaxesDomainMapper - extends ISequelizeDomainMapper< - CustomerInvoiceItemTaxModel, - CustomerInvoiceItemTaxCreationAttributes, - Tax - > {} - -export class ItemTaxesDomainMapper - extends SequelizeDomainMapper< - CustomerInvoiceItemTaxModel, - CustomerInvoiceItemTaxCreationAttributes, - Tax - > - implements IItemTaxesDomainMapper -{ - private _taxCatalog!: JsonTaxCatalogProvider; - - constructor(params: MapperParamsType) { - super(); - const { taxCatalog } = params as { - taxCatalog: JsonTaxCatalogProvider; - }; - - if (!taxCatalog) { - throw new Error('taxCatalog not defined ("ItemTaxesMapper")'); - } - - this._taxCatalog = taxCatalog; - } - - public mapToDomain( - source: CustomerInvoiceItemTaxModel, - params?: MapperParamsType - ): Result { - const { errors, index } = params as { - index: number; - errors: ValidationErrorDetail[]; - }; - - const tax = extractOrPushError( - Tax.createFromCode(source.tax_code, this._taxCatalog), - `taxes[${index}].tax_code`, - errors - ); - - // Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Invoice item tax mapping failed [mapToDomain]", errors) - ); - } - - // Creación del objeto de dominio - const createResult = Tax.create(tax!); - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice item tax creation failed", [ - { path: `taxes[${index}]`, message: createResult.error.message }, - ]) - ); - } - - return createResult; - } - - public mapToPersistence( - source: Tax, - params?: MapperParamsType - ): Result { - const { errors, parent } = params as { - parent: CustomerInvoiceItem; - errors: ValidationErrorDetail[]; - }; - - const taxableAmount = parent.getTaxableAmount(); - const taxAmount = taxableAmount.percentage(source.percentage); - - return Result.ok({ - tax_id: UniqueID.generateNewID().toPrimitive(), - item_id: parent.id.toPrimitive(), - - tax_code: source.code, - - taxable_amount_value: taxableAmount.value, - taxable_amount_scale: taxableAmount.scale, - - taxes_amount_value: taxAmount.value, - taxes_amount_scale: taxAmount.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 8037bb85..d00e4179 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,6 +22,7 @@ import { CustomerInvoiceStatus, InvoiceAmount, type InvoiceRecipient, + ItemAmount, type VerifactuRecord, } from "../../../domain"; import type { CustomerInvoiceModel } from "../../sequelize"; @@ -50,7 +51,11 @@ export type CustomerInvoiceListDTO = { languageCode: LanguageCode; currencyCode: CurrencyCode; - taxes: string; + taxes: { + tax_code: string; + taxable_amount: InvoiceAmount; + taxes_amount: InvoiceAmount; + }[]; discountPercentage: Percentage; @@ -103,7 +108,23 @@ export class CustomerInvoiceListMapper } // 3) Taxes - const taxes = raw.taxes.map((tax) => tax.tax_code).join(", "); + 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(); diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index 71453352..6bde4a2d 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -22,7 +22,6 @@ import type { import { CustomerInvoiceModel } from "./models/customer-invoice.model"; import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model"; -import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.model"; import { CustomerInvoiceTaxModel } from "./models/customer-invoice-tax.model"; import { VerifactuRecordModel } from "./models/verifactu-record.model"; @@ -102,12 +101,7 @@ export class CustomerInvoiceRepository // 3. Inserta items + sus taxes if (Array.isArray(items) && items.length > 0) { for (const item of items) { - const { taxes: itemTaxes, ...itemData } = item; - await CustomerInvoiceItemModel.create(itemData, { transaction }); - - if (Array.isArray(itemTaxes) && itemTaxes.length > 0) { - await CustomerInvoiceItemTaxModel.bulkCreate(itemTaxes, { transaction }); - } + await CustomerInvoiceItemModel.create(item, { transaction }); } } @@ -171,12 +165,7 @@ export class CustomerInvoiceRepository // 4. Inserta items + sus taxes if (Array.isArray(items) && items.length > 0) { for (const item of items) { - const { taxes: itemTaxes, ...itemData } = item; - await CustomerInvoiceItemModel.create(itemData, { transaction }); - - if (Array.isArray(itemTaxes) && itemTaxes.length > 0) { - await CustomerInvoiceItemTaxModel.bulkCreate(itemTaxes, { transaction }); - } + await CustomerInvoiceItemModel.create(item, { transaction }); } } @@ -276,13 +265,6 @@ export class CustomerInvoiceRepository model: CustomerInvoiceItemModel, as: "items", required: false, - include: [ - { - model: CustomerInvoiceItemTaxModel, - as: "taxes", - required: false, - }, - ], }, { model: CustomerInvoiceTaxModel, @@ -371,13 +353,6 @@ export class CustomerInvoiceRepository model: CustomerInvoiceItemModel, as: "items", required: false, - include: [ - { - model: CustomerInvoiceItemTaxModel, - as: "taxes", - required: false, - }, - ], }, { model: CustomerInvoiceTaxModel, @@ -484,7 +459,6 @@ export class CustomerInvoiceRepository as: "taxes", required: false, separate: true, // => query aparte, devuelve siempre array - attributes: ["tax_id", "tax_code"], }, ]; @@ -601,7 +575,6 @@ export class CustomerInvoiceRepository as: "taxes", required: false, separate: true, // => query aparte, devuelve siempre array - attributes: ["tax_id", "tax_code"], }, ]; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts index fdad48c6..40be26b4 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/index.ts @@ -1,6 +1,5 @@ import customerInvoiceModelInit from "./models/customer-invoice.model"; import customerInvoiceItemModelInit from "./models/customer-invoice-item.model"; -import customerInvoiceItemTaxesModelInit from "./models/customer-invoice-item-tax.model"; import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model"; import verifactuRecordModelInit from "./models/verifactu-record.model"; @@ -13,7 +12,6 @@ export const models = [ customerInvoiceItemModelInit, customerInvoiceTaxesModelInit, - customerInvoiceItemTaxesModelInit, verifactuRecordModelInit, ]; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item-tax.model.ts deleted file mode 100644 index 1a5a377e..00000000 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item-tax.model.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - DataTypes, - type InferAttributes, - type InferCreationAttributes, - Model, - type NonAttribute, - type Sequelize, -} from "sequelize"; - -import type { CustomerInvoiceItem } from "../../../domain"; - -export type CustomerInvoiceItemTaxCreationAttributes = InferCreationAttributes< - CustomerInvoiceItemTaxModel, - { omit: "item" } ->; - -export class CustomerInvoiceItemTaxModel extends Model< - InferAttributes, - InferCreationAttributes -> { - declare tax_id: string; - declare item_id: string; - - declare tax_code: string; //"iva_21" - - // Taxable amount (base imponible) // 100,00 € - declare taxable_amount_value: number; - declare taxable_amount_scale: number; - - // Total tax amount / taxes total // 21,00 € - declare taxes_amount_value: number; - declare taxes_amount_scale: number; - - // Relaciones - declare item: NonAttribute; - - static associate(database: Sequelize) { - const models = database.models; - - const requiredModels = ["CustomerInvoiceItemModel"]; - - // Comprobamos que los modelos existan - for (const name of requiredModels) { - if (!models[name]) { - throw new Error(`[CustomerInvoiceItemTaxModel.associate] Missing model: ${name}`); - } - } - - const { CustomerInvoiceItemModel } = models; - - CustomerInvoiceItemTaxModel.belongsTo(CustomerInvoiceItemModel, { - as: "item", - targetKey: "item_id", - foreignKey: "item_id", - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); - } - - static hooks(_database: Sequelize) { - // - } -} - -export default (database: Sequelize) => { - CustomerInvoiceItemTaxModel.init( - { - tax_id: { - type: DataTypes.UUID, - primaryKey: true, - }, - - item_id: { - type: DataTypes.UUID, - allowNull: false, - }, - - tax_code: { - type: new DataTypes.STRING(), - allowNull: false, - }, - - taxable_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - taxable_amount_scale: { - type: new 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, - }, - - taxes_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - }, - { - sequelize: database, - modelName: "CustomerInvoiceItemTaxModel", - tableName: "customer_invoice_item_taxes", - - underscored: true, - - indexes: [], - - whereMergeStrategy: "and", // <- cómo tratar el merge de un scope - - defaultScope: {}, - - scopes: {}, - } - ); - - return CustomerInvoiceItemTaxModel; -}; 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 7dcd0221..8648ab57 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 @@ -9,21 +9,15 @@ import { } from "sequelize"; import type { CustomerInvoiceModel } from "./customer-invoice.model"; -import type { - CustomerInvoiceItemTaxCreationAttributes, - CustomerInvoiceItemTaxModel, -} from "./customer-invoice-item-tax.model"; export type CustomerInvoiceItemCreationAttributes = InferCreationAttributes< CustomerInvoiceItemModel, - { omit: "invoice" | "taxes" } -> & { - taxes?: CustomerInvoiceItemTaxCreationAttributes[]; -}; + { omit: "invoice" } +>; export class CustomerInvoiceItemModel extends Model< InferAttributes, - InferCreationAttributes + InferCreationAttributes > { declare item_id: string; declare invoice_id: string; @@ -54,6 +48,39 @@ export class CustomerInvoiceItemModel extends Model< declare taxable_amount_value: CreationOptional; declare taxable_amount_scale: 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; @@ -64,15 +91,10 @@ export class CustomerInvoiceItemModel extends Model< // Relaciones declare invoice: NonAttribute; - declare taxes: NonAttribute; static associate(database: Sequelize) { const models = database.models; - const requiredModels = [ - "CustomerInvoiceModel", - "CustomerInvoiceItemModel", - "CustomerInvoiceItemTaxModel", - ]; + const requiredModels = ["CustomerInvoiceModel", "CustomerInvoiceItemModel"]; // Comprobamos que los modelos existan for (const name of requiredModels) { @@ -81,7 +103,7 @@ export class CustomerInvoiceItemModel extends Model< } } - const { CustomerInvoiceModel, CustomerInvoiceItemModel, CustomerInvoiceItemTaxModel } = models; + const { CustomerInvoiceModel, CustomerInvoiceItemModel } = models; CustomerInvoiceItemModel.belongsTo(CustomerInvoiceModel, { as: "invoice", @@ -90,15 +112,6 @@ export class CustomerInvoiceItemModel extends Model< onDelete: "CASCADE", onUpdate: "CASCADE", }); - - CustomerInvoiceItemModel.hasMany(CustomerInvoiceItemTaxModel, { - as: "taxes", - foreignKey: "item_id", - sourceKey: "item_id", - constraints: true, - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); } } @@ -199,6 +212,93 @@ export default (database: Sequelize) => { 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: true, @@ -230,7 +330,7 @@ export default (database: Sequelize) => { underscored: true, - indexes: [], + indexes: [{ fields: ["invoice_id"] }, { fields: ["invoice_id", "position"] }], whereMergeStrategy: "and", // <- cómo tratar el merge de un scope 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 98012d97..3c9ddc5e 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 @@ -49,9 +49,10 @@ export class CustomerInvoiceTaxModel extends Model< CustomerInvoiceTaxModel.belongsTo(CustomerInvoiceModel, { as: "invoice", - targetKey: "id", foreignKey: "invoice_id", + targetKey: "id", onDelete: "CASCADE", + onUpdate: "CASCADE", }); } @@ -74,7 +75,7 @@ export default (database: Sequelize) => { }, tax_code: { - type: new DataTypes.STRING(), + type: new DataTypes.STRING(40), // Sugerido por IA allowNull: false, }, @@ -113,7 +114,11 @@ export default (database: Sequelize) => { { name: "invoice_id_idx", fields: ["invoice_id"], - unique: false, + }, + { + name: "invoice_tax_code_unique", + fields: ["invoice_id", "tax_code"], + unique: true, // cada impuesto aparece como máximo una vez }, ], diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts index 4c131a10..137f8afb 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/index.ts @@ -1,5 +1,4 @@ export * from "./customer-invoice.model"; export * from "./customer-invoice-item.model"; -export * from "./customer-invoice-item-tax.model"; export * from "./customer-invoice-tax.model"; export * from "./verifactu-record.model"; 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 318c99d8..2f896989 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,7 +38,13 @@ export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema( country: z.string(), }), - taxes: z.string(), + taxes: z.array( + z.object({ + tax_code: z.string(), + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + }) + ), subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, 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 ed18c797..0a6b68ad 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,7 +38,13 @@ export const ListProformasResponseSchema = createPaginatedListSchema( country: z.string(), }), - taxes: z.string(), + taxes: z.array( + z.object({ + tax_code: z.string(), + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + }) + ), subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, diff --git a/modules/customer-invoices/src/web/issued-invoices/list/adapters/issued-invoice-summary-dto.adapter.ts b/modules/customer-invoices/src/web/issued-invoices/list/adapters/issued-invoice-summary-dto.adapter.ts index df2eb0a1..b3bb11c0 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/adapters/issued-invoice-summary-dto.adapter.ts +++ b/modules/customer-invoices/src/web/issued-invoices/list/adapters/issued-invoice-summary-dto.adapter.ts @@ -11,7 +11,6 @@ import type { */ export const IssuedInvoiceSummaryDtoAdapter = { fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData { - console.log(pageDto); return { ...pageDto, items: pageDto.items.map( diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx index d2c2f355..ebe15950 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx @@ -89,7 +89,7 @@ export function useIssuedInvoicesGridColumns( cell: ({ row }) => { const { verifactu } = row.original; const isPending = verifactu.status === "Pendiente"; - console.log(verifactu.status); + return ( <> {isPending ? ( @@ -331,12 +331,11 @@ export function useIssuedInvoicesGridColumns( return ( {/* Descargar en PDF */} - {/* Descargar en PDF */}