From 877d98f188389936f358f2be560f768960d1f4a1 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 10 Sep 2025 20:14:19 +0200 Subject: [PATCH] Facturas de cliente --- .../list-invoices-items.assembler.ts | 71 +++++++++ .../assembler/list-invoices.assembler.ts | 59 ++++++-- .../api/domain/aggregates/customer-invoice.ts | 137 +++++++----------- .../customer-invoice-items.ts | 15 +- .../entities/invoice-taxes/invoice-taxes.ts | 8 + .../customer-invoice-address-type.ts | 4 +- .../customer-invoice-item-description.ts | 4 +- .../value-objects/customer-invoice-number.ts | 4 +- .../value-objects/customer-invoice-serie.ts | 4 +- .../value-objects/customer-invoice-status.ts | 4 +- .../domain/value-objects/invoice-amount.ts | 51 ++++++- .../invoice-recipient/invoice-recipient.ts | 14 ++ .../api/domain/value-objects/item-amount.ts | 42 +++++- .../mappers/customer-invoice.mapper.ts | 52 ++----- .../mappers/invoice-recipient.mapper.ts | 12 ++ .../sequelize/customer-invoice.repository.ts | 6 + .../customer-invoices-list.response.dto.ts | 43 +++--- .../value-objects/customer-address-type.ts | 4 +- .../domain/value-objects/customer-number.ts | 4 +- .../domain/value-objects/customer-serie.ts | 4 +- .../domain/value-objects/customer-status.ts | 8 +- .../sequelize/customer.model.ts | 8 + packages/rdx-ddd/src/value-objects/city.ts | 4 + packages/rdx-ddd/src/value-objects/country.ts | 4 + .../src/value-objects/email-address.ts | 4 + packages/rdx-ddd/src/value-objects/name.ts | 4 + .../rdx-ddd/src/value-objects/phone-number.ts | 4 + .../src/value-objects/postal-address.ts | 21 +-- .../rdx-ddd/src/value-objects/postal-code.ts | 4 + .../rdx-ddd/src/value-objects/province.ts | 4 + packages/rdx-ddd/src/value-objects/slug.ts | 4 + packages/rdx-ddd/src/value-objects/street.ts | 4 + .../rdx-ddd/src/value-objects/tax-code.ts | 4 + .../rdx-ddd/src/value-objects/tin-number.ts | 4 + .../rdx-ddd/src/value-objects/url-address.ts | 4 + .../rdx-ddd/src/value-objects/utc-date.ts | 4 + packages/rdx-utils/src/helpers/index.ts | 1 + packages/rdx-utils/src/helpers/types.ts | 1 + 38 files changed, 443 insertions(+), 190 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts create mode 100644 packages/rdx-utils/src/helpers/types.ts diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts new file mode 100644 index 00000000..575b5ed4 --- /dev/null +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts @@ -0,0 +1,71 @@ +import { Criteria } from "@repo/rdx-criteria/server"; +import { toEmptyString } from "@repo/rdx-ddd"; +import { Collection } from "@repo/rdx-utils"; +import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; +import { CustomerInvoice } from "../../../domain"; + +export class ListCustomerInvoicesAssembler { + toDTO( + customerInvoices: Collection, + criteria: Criteria + ): CustomerInvoiceListResponseDTO { + const items = customerInvoices.map((invoice) => { + const recipient = invoice.recipient.match( + (recipient) => recipient.toString(), + () => ({ + tin: "", + name: "", + street: "", + street2: "", + city: "", + postal_code: "", + province: "", + country: "", + }) + ); + + return { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + customer_id: invoice.customerId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + recipient: { + customer_id: invoice.customerId.toString(), + ...recipient, + }, + + items, + + metadata: { + entity: "customer-invoice", + }, + }; + }); + + const totalItems = customerInvoices.total(); + + return { + page: criteria.pageNumber, + per_page: criteria.pageSize, + total_pages: Math.ceil(totalItems / criteria.pageSize), + total_items: totalItems, + items: items, + metadata: { + entity: "customer-invoices", + criteria: criteria.toJSON(), + //links: { + // self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`, + // first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`, + // last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`, + //}, + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts index 239fb275..90877b74 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts @@ -1,5 +1,6 @@ import { Criteria } from "@repo/rdx-criteria/server"; -import { Collection } from "@repo/rdx-utils"; +import { toEmptyString } from "@repo/rdx-ddd"; +import { ArrayElement, Collection } from "@repo/rdx-utils"; import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; import { CustomerInvoice } from "../../../domain"; @@ -8,27 +9,57 @@ export class ListCustomerInvoicesAssembler { customerInvoices: Collection, criteria: Criteria ): CustomerInvoiceListResponseDTO { - const items = customerInvoices.map((invoice) => { - return { - id: invoice.id.toPrimitive(), + const invoices = customerInvoices.map((invoice) => { + const recipientDTO = invoice.recipient.match( + (recipient) => recipient.toString(), + () => ({ + tin: "", + name: "", + street: "", + street2: "", + city: "", + postal_code: "", + province: "", + country: "", + }) + ); + + const allAmounts = invoice.getAllAmounts(); + + const invoiceDTO: ArrayElement = { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + customer_id: invoice.customerId.toString(), - invoice_status: invoice.status.toString(), invoice_number: invoice.invoiceNumber.toString(), - invoice_series: invoice.invoiceSeries.toString(), - invoice_date: invoice.invoiceDate.toISOString(), - operation_date: invoice.operationDate.toISOString(), - language_code: "ES", - currency: "EUR", + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), - subtotal_price: invoice.calculateSubtotal().toPrimitive(), - total_price: invoice.calculateTotal().toPrimitive(), + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), - //recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient), + recipient: { + customer_id: invoice.customerId.toString(), + ...recipientDTO, + }, + + taxes: invoice.taxes + .getAll() + .map((taxItem) => taxItem.tax.code) + .join(","), + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + discount_amount: allAmounts.discountAmount.toObjectString(), + taxable_amount: allAmounts.taxableAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(), metadata: { entity: "customer-invoice", }, }; + + return invoiceDTO; }); const totalItems = customerInvoices.total(); @@ -38,7 +69,7 @@ export class ListCustomerInvoicesAssembler { per_page: criteria.pageSize, total_pages: Math.ceil(totalItems / criteria.pageSize), total_items: totalItems, - items: items, + items: invoices, metadata: { entity: "customer-invoices", criteria: criteria.toJSON(), 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 dff16bb5..ddadea7a 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -3,7 +3,6 @@ import { AggregateRoot, CurrencyCode, LanguageCode, - MoneyValue, Percentage, TextValue, UniqueID, @@ -16,6 +15,7 @@ import { CustomerInvoiceNumber, CustomerInvoiceSerie, CustomerInvoiceStatus, + InvoiceAmount, InvoiceRecipient, } from "../value-objects"; @@ -38,22 +38,29 @@ export interface CustomerInvoiceProps { languageCode: LanguageCode; currencyCode: CurrencyCode; - //subtotalAmount: MoneyValue; + taxes: Maybe; + items: CustomerInvoiceItems; discountPercentage: Percentage; - //discountAmount: MoneyValue; +} - taxes: InvoiceTaxes; +export interface ICustomerInvoice { + getSubtotalAmount(): InvoiceAmount; + getDiscountAmount(): InvoiceAmount; - //totalAmount: MoneyValue; - - items: CustomerInvoiceItems; + getTaxableAmount(): InvoiceAmount; + getTaxesAmount(): InvoiceAmount; + getTotalAmount(): InvoiceAmount; } export type CustomerInvoicePatchProps = Partial>; -export class CustomerInvoice extends AggregateRoot { +export class CustomerInvoice + extends AggregateRoot + implements ICustomerInvoice +{ private _items!: CustomerInvoiceItems; + private _taxes!: InvoiceTaxes; protected constructor(props: CustomerInvoiceProps, id?: UniqueID) { super(props, id); @@ -63,6 +70,11 @@ export class CustomerInvoice extends AggregateRoot { languageCode: props.languageCode, currencyCode: props.currencyCode, }); + + this._taxes = props.taxes.match( + (taxes) => taxes, + () => InvoiceTaxes.create({}) + ); } static create(props: CustomerInvoiceProps, id?: UniqueID): Result { @@ -139,101 +151,60 @@ export class CustomerInvoice extends AggregateRoot { return this.props.currencyCode; } - public get subtotalAmount(): MoneyValue { - throw new Error("discountAmount not implemented"); - } - public get discountPercentage(): Percentage { return this.props.discountPercentage; } - public get discountAmount(): MoneyValue { - throw new Error("discountAmount not implemented"); - } - - public get taxableAmount(): MoneyValue { - throw new Error("taxableAmount not implemented"); - } - - public get taxAmount(): MoneyValue { - throw new Error("discountAmount not implemented"); - } - - public get totalAmount(): MoneyValue { - throw new Error("totalAmount not implemented"); - } - // Method to get the complete list of line items - get items(): CustomerInvoiceItems { + public get items(): CustomerInvoiceItems { return this._items; } - get hasRecipient() { + public get taxes(): InvoiceTaxes { + return this._taxes; + } + + public get hasRecipient() { return this.recipient.isSome(); } - /*get senderId(): UniqueID { - return this.props.senderId; - }*/ + public getSubtotalAmount(): InvoiceAmount { + const itemsSubtotal = this.items.getTotalAmount().convertScale(2); - /* get customer(): CustomerInvoiceCustomer | undefined { - return this.props.customer; - }*/ - - /*get purchareOrderNumber() { - return this.props.purchareOrderNumber; + return InvoiceAmount.create({ + value: itemsSubtotal.value, + currency_code: this.currencyCode.code, + }).data as InvoiceAmount; } - get paymentInstructions() { - return this.props.paymentInstructions; + public getDiscountAmount(): InvoiceAmount { + return this.getSubtotalAmount().percentage(this.discountPercentage) as InvoiceAmount; } - get paymentTerms() { - return this.props.paymentTerms; + public getTaxableAmount(): InvoiceAmount { + return this.getSubtotalAmount().subtract(this.getDiscountAmount()) as InvoiceAmount; } - get billTo() { - return this.props.billTo; + public getTaxesAmount(): InvoiceAmount { + return this._getTaxesAmount(this.getTaxableAmount()); } - get shipTo() { - return this.props.shipTo; - }*/ + public getTotalAmount(): InvoiceAmount { + const taxableAmount = this.getTaxableAmount(); + return taxableAmount.add(this._getTaxesAmount(taxableAmount)) as InvoiceAmount; + } - /* - addLineItem(lineItem: CustomerInvoiceLineItem, position?: number): void { - if (position === undefined) { - this._lineItems.push(lineItem); - } else { - this._lineItems.splice(position, 0, lineItem); - } - }*/ + public getAllAmounts() { + return { + subtotalAmount: this.getSubtotalAmount(), + discountAmount: this.getDiscountAmount(), + taxableAmount: this.getTaxableAmount(), + taxesAmount: this.getTaxesAmount(), + totalAmount: this.getTotalAmount(), + }; + } - /*calculateSubtotal(): MoneyValue { - const customerInvoiceSubtotal = MoneyValue.create({ - amount: 0, - currency_code: this.props.currency, - scale: 2, - }).data; - - return this._items.getAll().reduce((subtotal, item) => { - return subtotal.add(item.calculateTotal()); - }, customerInvoiceSubtotal); - }*/ - - // Method to calculate the total tax in the customerInvoice - /*calculateTaxTotal(): MoneyValue { - const taxTotal = MoneyValue.create({ - amount: 0, - currency_code: this.props.currency, - scale: 2, - }).data; - - return taxTotal; - }*/ - - // Method to calculate the total customerInvoice amount, including taxes - /*calculateTotal(): MoneyValue { - return this.calculateSubtotal().add(this.calculateTaxTotal()); - }*/ + private _getTaxesAmount(_taxableAmount: InvoiceAmount): InvoiceAmount { + return this._taxes.getTaxesAmount(_taxableAmount); + } } 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 59e35e56..d79bd499 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,5 +1,6 @@ import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; +import { ItemAmount } from "../../value-objects"; import { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { @@ -26,11 +27,19 @@ export class CustomerInvoiceItems extends Collection { 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)) { + if ( + !this._languageCode.equals(item.languageCode) || + !this._currencyCode.equals(item.currencyCode) + ) { return false; - } - return super.add(item) + return super.add(item); } + public getTotalAmount(): ItemAmount { + return this.getAll().reduce( + (total, tax) => total.add(tax.getTotalAmount()), + ItemAmount.zero(this._currencyCode.code) + ); + } } 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 d7bd9c90..aefd3b6f 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 { Collection } from "@repo/rdx-utils"; +import { InvoiceAmount } from "../../value-objects"; import { InvoiceTax } from "./invoice-tax"; export interface InvoiceTaxesProps { @@ -14,4 +15,11 @@ export class InvoiceTaxes extends Collection { public static create(props: InvoiceTaxesProps): InvoiceTaxes { return new InvoiceTaxes(props); } + + public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { + return this.getAll().reduce( + (total, tax) => total.add(tax.getTaxAmount(taxableAmount)), + InvoiceAmount.zero(taxableAmount.currencyCode) + ) as InvoiceAmount; + } } diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts index f731cf19..8a46a563 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts @@ -28,8 +28,8 @@ export class CustomerInvoiceAddressType extends ValueObject; @@ -11,7 +12,7 @@ export class InvoiceAmount extends MoneyValue { scale: InvoiceAmount.DEFAULT_SCALE, currency_code, }; - return MoneyValue.create(props); + return Result.ok(new InvoiceAmount(props)); } static zero(currency_code: string) { @@ -19,6 +20,50 @@ export class InvoiceAmount extends MoneyValue { value: 0, currency_code, }; - return InvoiceAmount.create(props); + return InvoiceAmount.create(props).data; + } + + toObjectString() { + return { + value: String(this.value), + scale: String(this.scale), + }; + } + + // Ensure fluent operations keep the subclass type + convertScale(newScale: number) { + const mv = super.convertScale(newScale); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); + } + + add(addend: MoneyValue) { + const mv = super.add(addend); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); + } + + subtract(subtrahend: MoneyValue) { + const mv = super.subtract(subtrahend); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); + } + + multiply(multiplier: number | Quantity) { + const mv = super.multiply(multiplier); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); + } + + divide(divisor: number | Quantity) { + const mv = super.divide(divisor); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); + } + + percentage(percentage: number | Percentage) { + const mv = super.percentage(percentage); + const p = mv.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code }); } } diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts index e5755705..ce2a0b54 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts @@ -7,6 +7,7 @@ import { Street, TINNumber, ValueObject, + toEmptyString, } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; @@ -84,4 +85,17 @@ export class InvoiceRecipient extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return { + tin: this.tin.toString(), + name: this.name.toString(), + street: toEmptyString(this.street, (value) => value.toString()), + street2: toEmptyString(this.street2, (value) => value.toString()), + city: toEmptyString(this.city, (value) => value.toString()), + postal_code: toEmptyString(this.postalCode, (value) => value.toString()), + province: toEmptyString(this.province, (value) => value.toString()), + country: toEmptyString(this.country, (value) => value.toString()), + }; + } } 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 bd0ab95d..01e9b9b3 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,5 @@ -import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; +import { MoneyValue, MoneyValueProps, Percentage, Quantity } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; type ItemAmountProps = Pick; @@ -11,7 +12,7 @@ export class ItemAmount extends MoneyValue { scale: ItemAmount.DEFAULT_SCALE, currency_code, }; - return MoneyValue.create(props); + return Result.ok(new ItemAmount(props)); } static zero(currency_code: string) { @@ -21,4 +22,41 @@ export class ItemAmount extends MoneyValue { }; return ItemAmount.create(props).data; } + + // Ensure fluent operations keep the subclass type + convertScale(newScale: number) { + const mv = super.convertScale(newScale); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } + + add(addend: MoneyValue) { + const mv = super.add(addend); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } + + subtract(subtrahend: MoneyValue) { + const mv = super.subtract(subtrahend); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } + + multiply(multiplier: number | Quantity) { + const mv = super.multiply(multiplier); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } + + divide(divisor: number | Quantity) { + const mv = super.divide(divisor); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } + + percentage(percentage: number | Percentage) { + const mv = super.percentage(percentage); + const p = mv.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code }); + } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts index 036845df..8d29715a 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts @@ -15,7 +15,7 @@ import { UtcDate, maybeFromNullableVO, } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceItems, @@ -118,13 +118,11 @@ export class CustomerInvoiceMapper ); const discountPercentage = extractOrPushError( - maybeFromNullableVO(source.discount_percentage_value, (value) => - Percentage.create({ - value: value, - scale: source.discount_percentage_scale, - }) - ), - "discount_percentage", + Percentage.create({ + value: source.discount_amount_scale, + scale: source.discount_percentage_scale, + }), + "discount_percentage_value", errors ); @@ -155,31 +153,7 @@ export class CustomerInvoiceMapper // 1) Valores escalares (atributos generales) const attributes = this.mapAttributesToDomain(source, { errors, ...params }); - // 2) Comprobar relaciones - const requireIncludes = Boolean(params?.requireIncludes); - if (requireIncludes) { - if (!source.items) { - errors.push({ - path: "items", - message: "Items not included in query (requireIncludes=true)", - }); - } - if (!source.taxes) { - errors.push({ - path: "taxes", - message: "Taxes not included in query (requireIncludes=true)", - }); - } - - if (attributes.isProforma && !source.current_customer) { - errors.push({ - path: "current_customer", - message: "Current customer not included in query (requireIncludes=true)", - }); - } - } - - // 3) Recipient (snapshot en la factura o include) + // 2) Recipient (snapshot en la factura o include) const recipientResult = this._recipientMapper.mapToDomain(source, { errors, attributes, @@ -194,7 +168,7 @@ export class CustomerInvoiceMapper }); } - // 4) Items (colección) + // 3) Items (colección) const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, { errors, attributes, @@ -208,7 +182,7 @@ export class CustomerInvoiceMapper }); } - // 5) Taxes (colección a nivel factura) + // 4) Taxes (colección a nivel factura) const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, { errors, attributes, @@ -222,14 +196,14 @@ export class CustomerInvoiceMapper }); } - // 6) Si hubo errores de mapeo, devolvemos colección de validación + // 5) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( new ValidationErrorCollection("Customer invoice mapping failed", errors) ); } - // 7) Construcción del agregado (Dominio) + // 6) Construcción del agregado (Dominio) const recipient = recipientResult.data; @@ -261,9 +235,9 @@ export class CustomerInvoiceMapper languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, - //discountPercentage: attributes.discountPercentage!, + discountPercentage: attributes.discountPercentage!, - taxes, + taxes: Maybe.some(taxes), items, }; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts index 4b61f069..a92aaa72 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts @@ -24,6 +24,11 @@ export class InvoiceRecipientMapper { source: CustomerInvoiceModel, params?: MapperParamsType ): Result, Error> { + /** + * - Factura === proforma -> datos de "current_customer" + * - Factura !== proforma -> snapshot de los datos (campos customer_*) + */ + const { errors, attributes } = params as { errors: ValidationErrorDetail[]; attributes: Partial; @@ -31,6 +36,13 @@ export class InvoiceRecipientMapper { const { isProforma } = attributes; + if (isProforma && !source.current_customer) { + errors.push({ + path: "current_customer", + message: "Current customer not included in query (InvoiceRecipientMapper)", + }); + } + const _name = isProforma ? source.current_customer.name : source.customer_name; const _tin = isProforma ? source.current_customer.tin : source.customer_tin; const _street = isProforma ? source.current_customer.street : source.customer_street; 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 2cd24ef7..a719f7ba 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 @@ -191,6 +191,12 @@ export class CustomerInvoiceRepository as: "current_customer", required: false, // false => LEFT JOIN }, + + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + }, ]; const instances = await CustomerInvoiceModel.findAll({ diff --git a/modules/customer-invoices/src/common/dto/response/customer-invoices-list.response.dto.ts b/modules/customer-invoices/src/common/dto/response/customer-invoices-list.response.dto.ts index e6ca0bc8..6c84a2c6 100644 --- a/modules/customer-invoices/src/common/dto/response/customer-invoices-list.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/customer-invoices-list.response.dto.ts @@ -1,28 +1,37 @@ -import { MetadataSchema, createListViewResponseSchema } from "@erp/core"; +import { AmountSchema, MetadataSchema, createListViewResponseSchema } from "@erp/core"; import * as z from "zod/v4"; export const CustomerInvoiceListResponseSchema = createListViewResponseSchema( z.object({ id: z.uuid(), - invoice_status: z.string(), + company_id: z.uuid(), + customer_id: z.string(), + invoice_number: z.string(), - invoice_series: z.string(), - invoice_date: z.iso.datetime({ offset: true }), - operation_date: z.iso.datetime({ offset: true }), - language_code: z.string(), - currency: z.string(), + status: z.string(), + series: z.string(), - subtotal_price: z.object({ - amount: z.number(), - scale: z.number(), - currency_code: z.string(), - }), + invoice_date: z.string(), + operation_date: z.string(), - total_price: z.object({ - amount: z.number(), - scale: z.number(), - currency_code: z.string(), - }), + recipient: { + tin: z.string(), + name: z.string(), + street: z.string(), + street2: z.string(), + city: z.string(), + postal_code: z.string(), + province: z.string(), + country: z.string(), + }, + + taxes: z.string(), + + subtotal_amount: AmountSchema, + discount_amount: AmountSchema, + taxable_amount: AmountSchema, + taxes_amount: AmountSchema, + total_amount: AmountSchema, metadata: MetadataSchema.optional(), }) diff --git a/modules/customers/src/api/domain/value-objects/customer-address-type.ts b/modules/customers/src/api/domain/value-objects/customer-address-type.ts index 82a62794..61529481 100644 --- a/modules/customers/src/api/domain/value-objects/customer-address-type.ts +++ b/modules/customers/src/api/domain/value-objects/customer-address-type.ts @@ -28,8 +28,8 @@ export class CustomerAddressType extends ValueObject return this.props.value; } - toString(): string { - return this.getProps(); + toString() { + return String(this.props.value); } toPrimitive(): string { diff --git a/modules/customers/src/api/domain/value-objects/customer-number.ts b/modules/customers/src/api/domain/value-objects/customer-number.ts index 3825ce13..d8ed16c3 100644 --- a/modules/customers/src/api/domain/value-objects/customer-number.ts +++ b/modules/customers/src/api/domain/value-objects/customer-number.ts @@ -38,8 +38,8 @@ export class CustomerNumber extends ValueObject { return this.props.value; } - toString(): string { - return this.getProps(); + toString() { + return String(this.props.value); } toPrimitive() { diff --git a/modules/customers/src/api/domain/value-objects/customer-serie.ts b/modules/customers/src/api/domain/value-objects/customer-serie.ts index c75fba6a..cab2a1d9 100644 --- a/modules/customers/src/api/domain/value-objects/customer-serie.ts +++ b/modules/customers/src/api/domain/value-objects/customer-serie.ts @@ -46,8 +46,8 @@ export class CustomerSerie extends ValueObject { return this.props.value; } - toString(): string { - return this.getProps(); + toString() { + return String(this.props.value); } toPrimitive() { diff --git a/modules/customers/src/api/domain/value-objects/customer-status.ts b/modules/customers/src/api/domain/value-objects/customer-status.ts index ce2c898d..d3ff0a2e 100644 --- a/modules/customers/src/api/domain/value-objects/customer-status.ts +++ b/modules/customers/src/api/domain/value-objects/customer-status.ts @@ -53,6 +53,10 @@ export class CustomerStatus extends ValueObject { return this.getProps(); } + toString() { + return String(this.props.value); + } + canTransitionTo(nextStatus: string): boolean { return CustomerStatus.TRANSITIONS[this.props.value].includes(nextStatus); } @@ -65,8 +69,4 @@ export class CustomerStatus extends ValueObject { } return CustomerStatus.create(nextStatus); } - - toString(): string { - return this.getProps(); - } } diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts index 5dd483c0..4497e8ac 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts @@ -39,6 +39,8 @@ export class CustomerModel extends Model< declare language_code: string; declare currency_code: string; + declare factuges_id: string; + static associate(database: Sequelize) {} static hooks(database: Sequelize) {} @@ -166,6 +168,12 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: "active", }, + + factuges_id: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + }, }, { sequelize: database, diff --git a/packages/rdx-ddd/src/value-objects/city.ts b/packages/rdx-ddd/src/value-objects/city.ts index 83019c13..d5202c8a 100644 --- a/packages/rdx-ddd/src/value-objects/city.ts +++ b/packages/rdx-ddd/src/value-objects/city.ts @@ -35,4 +35,8 @@ export class City extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/country.ts b/packages/rdx-ddd/src/value-objects/country.ts index fe2edd3f..de1a11bf 100644 --- a/packages/rdx-ddd/src/value-objects/country.ts +++ b/packages/rdx-ddd/src/value-objects/country.ts @@ -35,4 +35,8 @@ export class Country extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/email-address.ts b/packages/rdx-ddd/src/value-objects/email-address.ts index af90786e..c25f91ba 100644 --- a/packages/rdx-ddd/src/value-objects/email-address.ts +++ b/packages/rdx-ddd/src/value-objects/email-address.ts @@ -45,4 +45,8 @@ export class EmailAddress extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/name.ts b/packages/rdx-ddd/src/value-objects/name.ts index 41630a42..a94b328a 100644 --- a/packages/rdx-ddd/src/value-objects/name.ts +++ b/packages/rdx-ddd/src/value-objects/name.ts @@ -51,4 +51,8 @@ export class Name extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/phone-number.ts b/packages/rdx-ddd/src/value-objects/phone-number.ts index 8f386fdf..6eed5174 100644 --- a/packages/rdx-ddd/src/value-objects/phone-number.ts +++ b/packages/rdx-ddd/src/value-objects/phone-number.ts @@ -59,6 +59,10 @@ export class PhoneNumber extends ValueObject { return this.getProps(); } + toString() { + return String(this.props.value); + } + getCountryCode(): string | undefined { return parsePhoneNumberWithError(this.props.value).country; } diff --git a/packages/rdx-ddd/src/value-objects/postal-address.ts b/packages/rdx-ddd/src/value-objects/postal-address.ts index 3689f345..44517565 100644 --- a/packages/rdx-ddd/src/value-objects/postal-address.ts +++ b/packages/rdx-ddd/src/value-objects/postal-address.ts @@ -1,4 +1,5 @@ import { Maybe, Result } from "@repo/rdx-utils"; +import { toEmptyString } from "../helpers"; import { City } from "./city"; import { Country } from "./country"; import { PostalCode } from "./postal-code"; @@ -15,15 +16,6 @@ export interface PostalAddressProps { country: Maybe; } -export interface PostalAddressSnapshot { - street: string | null; - street2: string | null; - city: string | null; - postalCode: string | null; - province: string | null; - country: string | null; -} - export type PostalAddressPatchProps = Partial; export class PostalAddress extends ValueObject { @@ -81,6 +73,17 @@ export class PostalAddress extends ValueObject { return this.getProps(); } + toString() { + return { + street: toEmptyString(this.street, (value) => value.toString()), + street2: toEmptyString(this.street2, (value) => value.toString()), + city: toEmptyString(this.city, (value) => value.toString()), + postal_code: toEmptyString(this.postalCode, (value) => value.toString()), + province: toEmptyString(this.province, (value) => value.toString()), + country: toEmptyString(this.country, (value) => value.toString()), + }; + } + toFormat(): string { return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.province}, ${this.props.country}`; } diff --git a/packages/rdx-ddd/src/value-objects/postal-code.ts b/packages/rdx-ddd/src/value-objects/postal-code.ts index 40da0970..3043144e 100644 --- a/packages/rdx-ddd/src/value-objects/postal-code.ts +++ b/packages/rdx-ddd/src/value-objects/postal-code.ts @@ -43,4 +43,8 @@ export class PostalCode extends ValueObject { toPrimitive(): string { return this.props.value; } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/province.ts b/packages/rdx-ddd/src/value-objects/province.ts index d01c1de1..f963e1a9 100644 --- a/packages/rdx-ddd/src/value-objects/province.ts +++ b/packages/rdx-ddd/src/value-objects/province.ts @@ -35,4 +35,8 @@ export class Province extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/slug.ts b/packages/rdx-ddd/src/value-objects/slug.ts index a003e13a..54d4082d 100644 --- a/packages/rdx-ddd/src/value-objects/slug.ts +++ b/packages/rdx-ddd/src/value-objects/slug.ts @@ -39,4 +39,8 @@ export class Slug extends ValueObject { toPrimitive(): string { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/street.ts b/packages/rdx-ddd/src/value-objects/street.ts index 177c1b78..cf6421f5 100644 --- a/packages/rdx-ddd/src/value-objects/street.ts +++ b/packages/rdx-ddd/src/value-objects/street.ts @@ -35,4 +35,8 @@ export class Street extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/tax-code.ts b/packages/rdx-ddd/src/value-objects/tax-code.ts index 2f0aba9c..86b6f670 100644 --- a/packages/rdx-ddd/src/value-objects/tax-code.ts +++ b/packages/rdx-ddd/src/value-objects/tax-code.ts @@ -43,4 +43,8 @@ export class TaxCode extends ValueObject { toPrimitive(): string { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/tin-number.ts b/packages/rdx-ddd/src/value-objects/tin-number.ts index 172d388f..90108e0d 100644 --- a/packages/rdx-ddd/src/value-objects/tin-number.ts +++ b/packages/rdx-ddd/src/value-objects/tin-number.ts @@ -40,4 +40,8 @@ export class TINNumber extends ValueObject { toPrimitive(): string { return this.props.value; } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/url-address.ts b/packages/rdx-ddd/src/value-objects/url-address.ts index 934bf9b8..8ec57546 100644 --- a/packages/rdx-ddd/src/value-objects/url-address.ts +++ b/packages/rdx-ddd/src/value-objects/url-address.ts @@ -29,4 +29,8 @@ export class URLAddress extends ValueObject { toPrimitive() { return this.getProps(); } + + toString() { + return String(this.props.value); + } } diff --git a/packages/rdx-ddd/src/value-objects/utc-date.ts b/packages/rdx-ddd/src/value-objects/utc-date.ts index fe4de788..3f9983a4 100644 --- a/packages/rdx-ddd/src/value-objects/utc-date.ts +++ b/packages/rdx-ddd/src/value-objects/utc-date.ts @@ -58,6 +58,10 @@ export class UtcDate extends ValueObject { return this.date.toISOString().split("T")[0]; } + toString() { + return this.toDateString(); + } + /** * Devuelve la fecha en formato UTC con hora (ISO 8601). Ejemplo: 2025-12-31T23:59:59Z. */ diff --git a/packages/rdx-utils/src/helpers/index.ts b/packages/rdx-utils/src/helpers/index.ts index aa394e8d..4a86e3b7 100644 --- a/packages/rdx-utils/src/helpers/index.ts +++ b/packages/rdx-utils/src/helpers/index.ts @@ -5,4 +5,5 @@ export * from "./patch-field"; export * from "./result"; export * from "./result-collection"; export * from "./rule-validator"; +export * from "./types"; export * from "./utils"; diff --git a/packages/rdx-utils/src/helpers/types.ts b/packages/rdx-utils/src/helpers/types.ts new file mode 100644 index 00000000..ab869947 --- /dev/null +++ b/packages/rdx-utils/src/helpers/types.ts @@ -0,0 +1 @@ +export type ArrayElement = T extends readonly (infer U)[] ? U : never;