From e93d48b930ba30042350ff4a66f9593ac9f90523 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 26 Sep 2025 20:09:14 +0200 Subject: [PATCH] Clientes y facturas de cliente --- biome.json | 3 +- .../core/src/api/domain/value-objects/tax.ts | 6 +- .../src/api/domain/value-objects/taxes.ts | 13 +- .../mappers/sequelize-domain-mapper.ts | 41 ------ ...ap-dto-to-create-customer-invoice-props.ts | 4 +- .../api/domain/aggregates/customer-invoice.ts | 24 ++- .../customer-invoice-items.ts | 37 ++--- .../domain/entities/item-taxes/item-tax.ts | 36 ----- .../domain/entities/item-taxes/item-taxes.ts | 33 ++--- .../domain/customer-invoice-item.mapper.ts | 6 +- .../mappers/domain/customer-invoice.mapper.ts | 137 ++++++++++-------- .../mappers/domain/invoice-taxes.mapper.ts | 79 +++++----- .../mappers/domain/item-taxes.mapper.ts | 20 +-- .../models/customer-invoice-item-tax.model.ts | 12 +- .../models/customer-invoice-item.model.ts | 20 +-- .../models/customer-invoice-tax.model.ts | 8 +- .../models/customer-invoice.model.ts | 73 ++++++---- .../src/api/domain/aggregates/customer.ts | 4 + .../mappers/domain/customer.mapper.ts | 2 +- .../sequelize/models/customer.model.ts | 65 +++++---- packages/rdx-ddd/src/helpers/normalizers.ts | 7 +- packages/rdx-utils/src/helpers/collection.ts | 6 +- 22 files changed, 290 insertions(+), 346 deletions(-) delete mode 100644 modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts diff --git a/biome.json b/biome.json index 03735ab9..3b48da29 100644 --- a/biome.json +++ b/biome.json @@ -26,7 +26,8 @@ "noForEach": "off", "noBannedTypes": "info", "noUselessFragments": "off", - "useOptionalChain": "off" + "useOptionalChain": "off", + "noThisInStatic": "off" }, "suspicious": { "noImplicitAnyLet": "info", diff --git a/modules/core/src/api/domain/value-objects/tax.ts b/modules/core/src/api/domain/value-objects/tax.ts index d1027587..1c3486a0 100644 --- a/modules/core/src/api/domain/value-objects/tax.ts +++ b/modules/core/src/api/domain/value-objects/tax.ts @@ -141,11 +141,6 @@ export class Tax extends ValueObject { return `${this.toNumber().toFixed(this.scale)}%`; } - /** Calcula el importe del impuesto sobre una base imponible */ - calculateAmount(baseAmount: number): number { - return (baseAmount * this.toNumber()) / 100; - } - isZero(): boolean { return this.toNumber() === 0; } @@ -159,6 +154,7 @@ export class Tax extends ValueObject { greaterThan(other: Tax): boolean { return this.toNumber() > other.toNumber(); } + lessThan(other: Tax): boolean { return this.toNumber() < other.toNumber(); } diff --git a/modules/core/src/api/domain/value-objects/taxes.ts b/modules/core/src/api/domain/value-objects/taxes.ts index 721aeaa7..71c7bbc0 100644 --- a/modules/core/src/api/domain/value-objects/taxes.ts +++ b/modules/core/src/api/domain/value-objects/taxes.ts @@ -1,17 +1,8 @@ import { Collection } from "@repo/rdx-utils"; import { Tax } from "./tax"; -export interface TaxesProps { - items?: Tax[]; -} - export class Taxes extends Collection { - constructor(props: TaxesProps) { - const { items } = props; - super(items); - } - - public static create(props: TaxesProps): Taxes { - return new Taxes(props); + public static create(this: new (items: Tax[]) => T, items: Tax[]): T { + return new this(items); } } diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts index 6457833d..a196f383 100644 --- a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts +++ b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts @@ -44,45 +44,4 @@ export abstract class SequelizeDomainMapper(operation: () => T, key: string): Result { - try { - return Result.ok(operation()); - } catch (error: unknown) { - return Result.fail(error as Error); - } - } - - protected _mapsValue( - row: TModel, - key: string, - customMapFn: (value: any, params: MapperParamsType) => Result, - params: MapperParamsType = { defaultValue: null } - ): Result { - return customMapFn(row?.dataValues[key] ?? params.defaultValue, params); - } - - protected _mapsAssociation( - row: TModel, - associationName: string, - customMapper: DomainMapperWithBulk, - params: MapperParamsType = {} - ): Result { - if (!customMapper) { - Result.fail(Error(`Custom mapper undefined for ${associationName}`)); - } - - const { filter, ...otherParams } = params; - let associationRows = row?.dataValues[associationName] ?? []; - - if (filter) { - associationRows = Array.isArray(associationRows) - ? associationRows.filter(filter) - : filter(associationRows); - } - - return Array.isArray(associationRows) - ? customMapper.mapToDomainCollection(associationRows, associationRows.length, otherParams) - : customMapper.mapToDomain(associationRows, otherParams); - }*/ } diff --git a/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts index f35a81bc..682a32da 100644 --- a/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts +++ b/modules/customer-invoices/src/api/application/use-cases/create/map-dto-to-create-customer-invoice-props.ts @@ -149,7 +149,7 @@ export class CreateCustomerInvoicePropsMapper { discountPercentage: discountPercentage!, - taxes: Taxes.create({ items: [] }), + taxes: Taxes.create([]), items: items, }; @@ -219,7 +219,7 @@ export class CreateCustomerInvoicePropsMapper { } private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) { - const taxes = Taxes.create({ items: [] }); + const taxes = Taxes.create([]); item.taxes.split(",").every((tax_code, taxIndex) => { const taxResult = Tax.createFromCode(tax_code, this.taxCatalog); 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 aa9ed6fb..5c8d5aeb 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -10,7 +10,6 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoiceItems, InvoicePaymentMethod } from "../entities"; -import { InvoiceTaxes } from "../entities/invoice-taxes"; import { CustomerInvoiceNumber, CustomerInvoiceSerie, @@ -23,9 +22,10 @@ export interface CustomerInvoiceProps { companyId: UniqueID; isProforma: boolean; - invoiceNumber: CustomerInvoiceNumber; status: CustomerInvoiceStatus; + series: Maybe; + invoiceNumber: Maybe; invoiceDate: UtcDate; operationDate: Maybe; @@ -33,6 +33,7 @@ export interface CustomerInvoiceProps { customerId: UniqueID; recipient: Maybe; + reference: Maybe; notes: Maybe; languageCode: LanguageCode; @@ -44,9 +45,9 @@ export interface CustomerInvoiceProps { discountPercentage: Percentage; - verifactu_qr: string; + /*verifactu_qr: string; verifactu_url: string; - verifactu_status: string; + verifactu_status: string;*/ } export interface ICustomerInvoice { @@ -139,6 +140,10 @@ export class CustomerInvoice return this.props.operationDate; } + public get reference(): Maybe { + return this.props.reference; + } + public get notes(): Maybe { return this.props.notes; } @@ -168,7 +173,7 @@ export class CustomerInvoice return this._items; } - public get taxes(): InvoiceTaxes { + public get taxes() { return this.items.getTaxesAmountByTaxes(); } @@ -192,7 +197,12 @@ export class CustomerInvoice } private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { - return this._taxes.getTaxesAmount(taxableAmount); + let amount = InvoiceAmount.zero(this.currencyCode.code); + + for (const tax of this.taxes) { + amount = amount.add(tax.taxesAmount); + } + return amount; } private _getTotalAmount(taxableAmount: InvoiceAmount, taxesAmount: InvoiceAmount): InvoiceAmount { @@ -249,7 +259,7 @@ export class CustomerInvoice ...this.props, status: CustomerInvoiceStatus.createEmitted(), isProforma: false, - invoiceNumber: newInvoiceNumber, + invoiceNumber: Maybe.some(newInvoiceNumber), }, this.id ); 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 e1f1e218..32cbdb54 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 @@ -2,7 +2,6 @@ import { Tax } from "@erp/core/api"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount } from "../../value-objects"; -import { InvoiceTax, InvoiceTaxes } from "../invoice-taxes"; import { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { @@ -73,31 +72,33 @@ export class CustomerInvoiceItems extends Collection { ); } - public getTaxesAmountByTaxes(): InvoiceTaxes { - InvoiceTaxes.create({}); + public getTaxesAmountByTaxes() { + const resultMap = new Map(); - const taxesMap = new Map(); const currencyCode = this._currencyCode.code; for (const item of this.getAll()) { - for (const { tax, taxesAmount } of item.getTaxesAmountByTaxes()) { - const current = taxesMap.get(tax) ?? ItemAmount.zero(currencyCode); - taxesMap.set(tax, current.add(taxesAmount)); + for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) { + const { taxableAmount: taxableCurrent, taxesAmount: taxesCurrent } = resultMap.get(tax) ?? { + taxableAmount: ItemAmount.zero(currencyCode), + taxesAmount: ItemAmount.zero(currencyCode), + }; + resultMap.set(tax, { + taxableAmount: taxableCurrent.add(taxableAmount), + taxesAmount: taxesCurrent.add(taxesAmount), + }); } } - const items: InvoiceTax[] = []; - for (const [tax, taxesAmount] of taxesMap) { - items.push( - InvoiceTax.create({ - tax, - taxesAmount, - }).data - ); + const items = []; + for (const [tax, { taxableAmount, taxesAmount }] of resultMap) { + items.push({ + taxableAmount, + tax, + taxesAmount, + }); } - return InvoiceTaxes.create({ - items: items, - }); + return items; } } diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts deleted file mode 100644 index 96c396dd..00000000 --- a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Tax } from "@erp/core/api"; -import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { ItemAmount } from "../../value-objects"; - -export interface ItemTaxProps { - tax: Tax; -} - -export class ItemTax extends DomainEntity { - static create(props: ItemTaxProps, id?: UniqueID): Result { - const itemTax = new ItemTax(props, id); - - // Reglas de negocio / validaciones - // ... - // ... - - return Result.ok(itemTax); - } - - public get tax(): Tax { - return this.props.tax; - } - - getProps(): ItemTaxProps { - return this.props; - } - - toPrimitive() { - return this.getProps(); - } - - public getTaxAmount(taxableAmount: ItemAmount): ItemAmount { - return taxableAmount.percentage(this.tax.percentage); - } -} 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 index 05dfe704..7051c455 100644 --- 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 @@ -1,24 +1,14 @@ -import { Collection } from "@repo/rdx-utils"; +import { Tax, Taxes } from "@erp/core/api"; import { ItemAmount } from "../../value-objects"; -import { ItemTax } from "./item-tax"; -export interface ItemTaxesProps { - items?: ItemTax[]; -} - -export class ItemTaxes extends Collection { - constructor(props: ItemTaxesProps) { - const { items = [] } = props; - super(items); - } - - public static create(props: ItemTaxesProps): ItemTaxes { - return new ItemTaxes(props); +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(tax.getTaxAmount(taxableAmount)), + (total, tax) => total.add(taxableAmount.percentage(tax.percentage)), ItemAmount.zero(taxableAmount.currencyCode) ); } @@ -26,21 +16,22 @@ export class ItemTaxes extends Collection { public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount { const currencyCode = taxableAmount.currencyCode; - return this.filter((itemTax) => itemTax.tax.code === taxCode).reduce((totalAmount, itemTax) => { - return itemTax.getTaxAmount(taxableAmount).add(totalAmount); + return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => { + return taxableAmount.percentage(itemTax.percentage).add(totalAmount); }, ItemAmount.zero(currencyCode)); } - public getTaxesAmountByTaxes(taxableAmount: ItemAmount): { + public getTaxesAmountByTaxes(taxableAmount: ItemAmount) { return this.getAll().map((taxItem) => ({ - tax: taxItem.tax, - taxesAmount: this.getTaxesAmountByTaxCode(taxItem.tax.code, taxableAmount), + taxableAmount, + tax: taxItem, + taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount), })); } public getCodesToString(): string { return this.getAll() - .map((taxItem) => taxItem.tax.code) + .map((taxItem) => taxItem.code) .join(", "); } } 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 5a1cbaa8..846b2fe6 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 @@ -146,9 +146,7 @@ export class CustomerInvoiceItemDomainMapper // 5) Construcción del elemento de dominio - const taxes = ItemTaxes.create({ - items: taxesResults.data.getAll(), - }); + const taxes = ItemTaxes.create(taxesResults.data.getAll()); const createResult = CustomerInvoiceItem.create( { @@ -231,6 +229,8 @@ export class CustomerInvoiceItemDomainMapper total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, + + taxes: taxesResults.data, }); } } 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 2515700e..77604846 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 @@ -1,9 +1,4 @@ -import { - ISequelizeDomainMapper, - InfrastructureError, - MapperParamsType, - SequelizeDomainMapper, -} from "@erp/core/api"; +import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { CurrencyCode, LanguageCode, @@ -17,7 +12,7 @@ import { maybeFromNullableVO, toNullable, } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; +import { Collection, Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceItems, @@ -27,7 +22,6 @@ import { CustomerInvoiceStatus, InvoicePaymentMethod, } from "../../../domain"; -import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceItemDomainMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.mapper"; import { InvoiceRecipientDomainMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.mapper"; @@ -132,7 +126,7 @@ export class CustomerInvoiceDomainMapper ); const invoiceNumber = extractOrPushError( - CustomerInvoiceNumber.create(source.invoice_number), + maybeFromNullableVO(source.invoice_number, (value) => CustomerInvoiceNumber.create(value)), "invoice_number", errors ); @@ -149,6 +143,12 @@ export class CustomerInvoiceDomainMapper errors ); + const reference = extractOrPushError( + maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))), + "reference", + errors + ); + const notes = extractOrPushError( maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), "notes", @@ -186,6 +186,7 @@ export class CustomerInvoiceDomainMapper invoiceNumber, invoiceDate, operationDate, + reference, notes, languageCode, currencyCode, @@ -274,10 +275,6 @@ export class CustomerInvoiceDomainMapper const recipient = recipientResult.data; const paymentMethod = paymentMethodResult.data; - const taxes = InvoiceTaxes.create({ - items: taxesResults.data.getAll(), - }); - const items = CustomerInvoiceItems.create({ languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, @@ -297,6 +294,7 @@ export class CustomerInvoiceDomainMapper customerId: attributes.customerId!, recipient: recipient, + reference: attributes.reference!, notes: attributes.notes!, languageCode: attributes.languageCode!, @@ -306,7 +304,6 @@ export class CustomerInvoiceDomainMapper paymentMethod: paymentMethod!, - taxes: taxes, items, }; @@ -345,8 +342,11 @@ export class CustomerInvoiceDomainMapper }); } + const items = itemsResult.data; + // 1) Taxes - const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, { + + const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.taxes), { errors, parent: source, ...params, @@ -358,18 +358,31 @@ export class CustomerInvoiceDomainMapper }); } + const taxes = taxesResult.data; + // 3) Calcular totales const allAmounts = source.getAllAmounts(); - // 4) Construir parte - const invoiceValues: Partial = { + // 4) Cliente + const recipient = this._mapRecipientToPersistence(source); + + // 7) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } + + const invoiceValues: CustomerInvoiceCreationAttributes = { id: source.id.toPrimitive(), company_id: source.companyId.toPrimitive(), is_proforma: source.isProforma, status: source.status.toPrimitive(), series: toNullable(source.series, (series) => series.toPrimitive()), - invoice_number: source.invoiceNumber.toPrimitive(), + invoice_number: toNullable(source.invoiceNumber, (invoiceNumber) => + invoiceNumber.toPrimitive() + ), invoice_date: source.invoiceDate.toPrimitive(), operation_date: toNullable(source.operationDate, (operationDate) => operationDate.toPrimitive() @@ -377,6 +390,7 @@ export class CustomerInvoiceDomainMapper language_code: source.languageCode.toPrimitive(), currency_code: source.currencyCode.toPrimitive(), + reference: toNullable(source.reference, (reference) => reference), notes: toNullable(source.notes, (notes) => notes.toPrimitive()), subtotal_amount_value: allAmounts.subtotalAmount.value, @@ -397,61 +411,58 @@ export class CustomerInvoiceDomainMapper total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, - customer_id: source.customerId.toPrimitive(), - payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id), payment_method_description: toNullable( source.paymentMethod, (payment) => payment.toObjectString().payment_description ), + + customer_id: source.customerId.toPrimitive(), + ...recipient, + taxes, + items, }; - // 5) Cliente / Recipient ?? - // Si es proforma no guardamos los campos como históricos (snapshots) - if (source.isProforma) { - Object.assign(invoiceValues, { - customer_tin: null, - customer_name: null, - customer_street: null, - customer_street2: null, - customer_city: null, - customer_province: null, - customer_postal_code: null, - customer_country: null, + return Result.ok(invoiceValues); + } + + protected _mapRecipientToPersistence(source: CustomerInvoice, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const recipient = source.recipient.getOrUndefined(); + + if (!source.isProforma && !recipient) { + errors.push({ + path: "recipient", + message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data", }); - } else { - const recipient = source.recipient.getOrUndefined(); - if (!recipient) { - return Result.fail( - new InfrastructureError( - "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data" - ) - ); - } - - Object.assign(invoiceValues, { - customer_tin: recipient.tin.toPrimitive(), - customer_name: recipient.name.toPrimitive(), - customer_street: toNullable(recipient.street, (v) => v.toPrimitive()), - customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()), - customer_city: toNullable(recipient.city, (v) => v.toPrimitive()), - customer_province: toNullable(recipient.province, (v) => v.toPrimitive()), - customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()), - customer_country: toNullable(recipient.country, (v) => v.toPrimitive()), - } as Partial); } - // 7) Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) - ); - } + const recipientValues = { + customer_tin: !source.isProforma ? recipient!.tin.toPrimitive() : null, + customer_name: !source.isProforma ? recipient!.name.toPrimitive() : null, + customer_street: !source.isProforma + ? toNullable(recipient!.street, (v) => v.toPrimitive()) + : null, + customer_street2: !source.isProforma + ? toNullable(recipient!.street2, (v) => v.toPrimitive()) + : null, + customer_city: !source.isProforma + ? toNullable(recipient!.city, (v) => v.toPrimitive()) + : null, + customer_province: !source.isProforma + ? toNullable(recipient!.province, (v) => v.toPrimitive()) + : null, + customer_postal_code: !source.isProforma + ? toNullable(recipient!.postalCode, (v) => v.toPrimitive()) + : null, + customer_country: !source.isProforma + ? toNullable(recipient!.country, (v) => v.toPrimitive()) + : null, + }; - return Result.ok({ - ...invoiceValues, - items: itemsResult.data, - taxes: taxesResult.data, - }); + return recipientValues; } } 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 f3b32f42..3d982627 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,21 +1,15 @@ import { JsonTaxCatalogProvider } from "@erp/core"; import { MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; -import { - ValidationErrorCollection, - ValidationErrorDetail, - extractOrPushError, -} from "@repo/rdx-ddd"; +import { UniqueID, ValidationErrorDetail } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { InferCreationAttributes } from "sequelize"; -import { CustomerInvoice, CustomerInvoiceProps } from "../../../domain"; -import { InvoiceTax } from "../../../domain/entities/invoice-taxes"; +import { CustomerInvoice, CustomerInvoiceItemProps, ItemAmount } from "../../../domain"; import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize"; export class TaxesDomainMapper extends SequelizeDomainMapper< CustomerInvoiceTaxModel, CustomerInvoiceTaxCreationAttributes, - InvoiceTax + { taxableAmount: ItemAmount; tax: Tax; taxesAmount: ItemAmount } > { private _taxCatalog: JsonTaxCatalogProvider; @@ -35,60 +29,57 @@ export class TaxesDomainMapper extends SequelizeDomainMapper< public mapToDomain( source: CustomerInvoiceTaxModel, params?: MapperParamsType - ): Result { - const { errors, index, attributes } = params as { - index: number; - errors: ValidationErrorDetail[]; - attributes: Partial; + ): Result< + { + taxableAmount: ItemAmount; + tax: Tax; + taxesAmount: ItemAmount; + }, + Error + > { + const { attributes } = params as { + attributes: Partial; }; - const tax = extractOrPushError( - Tax.createFromCode(source.tax_code, this._taxCatalog), - `taxes[${index}].tax_code`, - errors - ); - - // Creación del objeto de dominio - const createResult = InvoiceTax.create({ - tax: tax!, + return Result.ok({ + taxableAmount: ItemAmount.create({ + value: source.taxable_amount_value, + currency_code: attributes.currencyCode!.code, + }).data, + tax: Tax.createFromCode(source.tax_code, this._taxCatalog).data, + taxesAmount: ItemAmount.create({ + value: source.taxes_amount_value, + currency_code: attributes.currencyCode!.code, + }).data, }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice tax creation failed", [ - { path: `taxes[${index}]`, message: createResult.error.message }, - ]) - ); - } - - return createResult; } public mapToPersistence( - source: InvoiceTax, + source: { + taxableAmount: ItemAmount; + tax: Tax; + taxesAmount: ItemAmount; + }, params?: MapperParamsType - ): Result, Error> { + ): Result { const { errors, parent } = params as { - index: number; parent: CustomerInvoice; errors: ValidationErrorDetail[]; }; - - const taxableAmount = parent.getTaxableAmount() - const taxesAmount = + source; return Result.ok({ - tax_id: source.id.toPrimitive(), + tax_id: UniqueID.generateNewID().toPrimitive(), invoice_id: parent.id.toPrimitive(), tax_code: source.tax.code, - taxable_amount_value: taxableAmount.value, - taxable_amount_scale: taxableAmount.scale, + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, - taxes_amount_value: taxesAmount.value, - taxes_amount_scale: taxesAmount.scale, + taxes_amount_value: source.taxesAmount.value, + taxes_amount_scale: source.taxesAmount.scale, }); } } 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 index 872a450f..28621515 100644 --- 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 @@ -5,13 +5,15 @@ import { SequelizeDomainMapper, Tax, } from "@erp/core/api"; -import { CustomerInvoiceItem, ItemTax } from "@erp/customer-invoices/api/domain"; + import { + UniqueID, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; +import { CustomerInvoiceItem } from "../../../domain"; import { CustomerInvoiceItemTaxCreationAttributes, CustomerInvoiceItemTaxModel, @@ -21,14 +23,14 @@ export interface IItemTaxesDomainMapper extends ISequelizeDomainMapper< CustomerInvoiceItemTaxModel, CustomerInvoiceItemTaxCreationAttributes, - ItemTax + Tax > {} export class ItemTaxesDomainMapper extends SequelizeDomainMapper< CustomerInvoiceItemTaxModel, CustomerInvoiceItemTaxCreationAttributes, - ItemTax + Tax > implements IItemTaxesDomainMapper { @@ -50,7 +52,7 @@ export class ItemTaxesDomainMapper public mapToDomain( source: CustomerInvoiceItemTaxModel, params?: MapperParamsType - ): Result { + ): Result { const { errors, index } = params as { index: number; errors: ValidationErrorDetail[]; @@ -63,7 +65,7 @@ export class ItemTaxesDomainMapper ); // Creación del objeto de dominio - const createResult = ItemTax.create({ tax: tax! }); + const createResult = Tax.create(tax!); if (createResult.isFailure) { return Result.fail( new ValidationErrorCollection("Invoice item tax creation failed", [ @@ -76,7 +78,7 @@ export class ItemTaxesDomainMapper } public mapToPersistence( - source: ItemTax, + source: Tax, params?: MapperParamsType ): Result { const { errors, parent } = params as { @@ -85,13 +87,13 @@ export class ItemTaxesDomainMapper }; const taxableAmount = parent.getTaxableAmount(); - const taxAmount = source.getTaxAmount(taxableAmount); + const taxAmount = taxableAmount.percentage(source.percentage); return Result.ok({ - tax_id: source.id.toPrimitive(), + tax_id: UniqueID.generateNewID().toPrimitive(), item_id: parent.id.toPrimitive(), - tax_code: source.tax.code, + tax_code: source.code, taxable_amount_value: taxableAmount.value, taxable_amount_scale: taxableAmount.scale, 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 index 02dfe97f..0749776e 100644 --- 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 @@ -68,26 +68,26 @@ export default (database: Sequelize) => { taxable_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxable_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, - defaultValue: 2, + defaultValue: 4, }, taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxes_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, - defaultValue: 2, + defaultValue: 4, }, }, { 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 ea6f87a9..95db8728 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 @@ -1,4 +1,5 @@ import { + CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, @@ -28,38 +29,39 @@ export class CustomerInvoiceItemModel extends Model< declare position: number; - declare description: string; + declare description: CreationOptional; - declare quantity_value: number; + declare quantity_value: CreationOptional; declare quantity_scale: number; - declare unit_amount_value: number; + declare unit_amount_value: CreationOptional; declare unit_amount_scale: number; // Subtotal - declare subtotal_amount_value: number; + declare subtotal_amount_value: CreationOptional; declare subtotal_amount_scale: number; // Discount percentage - declare discount_percentage_value: number; + declare discount_percentage_value: CreationOptional; declare discount_percentage_scale: number; // Discount amount - declare discount_amount_value: number; + declare discount_amount_value: CreationOptional; declare discount_amount_scale: number; // Taxable amount (base imponible) - declare taxable_amount_value: number; + declare taxable_amount_value: CreationOptional; declare taxable_amount_scale: number; // Total taxes amount / taxes total - declare taxes_amount_value: number; + declare taxes_amount_value: CreationOptional; declare taxes_amount_scale: number; // Total - declare total_amount_value: number; + declare total_amount_value: CreationOptional; declare total_amount_scale: number; + // Relaciones declare invoice: NonAttribute; declare taxes: NonAttribute; 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 a680547b..b2d44146 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 @@ -67,8 +67,8 @@ export default (database: Sequelize) => { taxable_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxable_amount_scale: { @@ -79,8 +79,8 @@ export default (database: Sequelize) => { taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxes_amount_scale: { diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts index cf3ba4f6..220b1f1f 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts @@ -1,4 +1,5 @@ import { + CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, @@ -34,15 +35,20 @@ export class CustomerInvoiceModel extends Model< declare is_proforma: boolean; declare status: string; - declare series: string; - declare invoice_number: string; - declare invoice_date: string; - declare operation_date: string; - declare language_code: string; - declare currency_code: string; - //declare xxxxxx + declare series: CreationOptional; + declare invoice_number: CreationOptional; + declare invoice_date: CreationOptional; + declare operation_date: CreationOptional; + declare language_code: CreationOptional; + declare currency_code: CreationOptional; - declare notes: string; + declare reference: CreationOptional; + + declare notes: CreationOptional; + + // Método de pago + declare payment_method_id: CreationOptional; + declare payment_method_description: CreationOptional; // Subtotal declare subtotal_amount_value: number; @@ -70,18 +76,14 @@ export class CustomerInvoiceModel extends Model< // Customer declare customer_id: string; - declare customer_tin: string; - declare customer_name: string; - declare customer_street: string; - declare customer_street2: string; - declare customer_city: string; - declare customer_province: string; - declare customer_postal_code: string; - declare customer_country: string; - - // Método de pago - declare payment_method_id: string; - declare payment_method_description: string; + declare customer_tin: CreationOptional; + declare customer_name: CreationOptional; + declare customer_street: CreationOptional; + declare customer_street2: CreationOptional; + declare customer_city: CreationOptional; + declare customer_province: CreationOptional; + declare customer_postal_code: CreationOptional; + declare customer_country: CreationOptional; // Relaciones declare items: NonAttribute; @@ -163,8 +165,7 @@ export default (database: Sequelize) => { invoice_date: { type: new DataTypes.DATEONLY(), - allowNull: true, - defaultValue: null, + allowNull: false, }, operation_date: { @@ -182,6 +183,13 @@ export default (database: Sequelize) => { currency_code: { type: new DataTypes.STRING(3), allowNull: false, + defaultValue: "EUR", + }, + + reference: { + type: new DataTypes.STRING(), + allowNull: true, + defaultValue: null, }, notes: { @@ -207,6 +215,7 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: 0, }, + subtotal_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, @@ -215,8 +224,8 @@ export default (database: Sequelize) => { discount_percentage_value: { type: new DataTypes.SMALLINT(), - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, discount_percentage_scale: { @@ -227,8 +236,8 @@ export default (database: Sequelize) => { discount_amount_value: { type: new DataTypes.BIGINT(), - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, discount_amount_scale: { @@ -239,8 +248,8 @@ export default (database: Sequelize) => { taxable_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxable_amount_scale: { type: new DataTypes.SMALLINT(), @@ -250,8 +259,8 @@ export default (database: Sequelize) => { taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, taxes_amount_scale: { type: new DataTypes.SMALLINT(), @@ -261,8 +270,8 @@ export default (database: Sequelize) => { total_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: 0, }, total_amount_scale: { diff --git a/modules/customers/src/api/domain/aggregates/customer.ts b/modules/customers/src/api/domain/aggregates/customer.ts index 58c1d74a..ea304a99 100644 --- a/modules/customers/src/api/domain/aggregates/customer.ts +++ b/modules/customers/src/api/domain/aggregates/customer.ts @@ -30,14 +30,18 @@ export interface CustomerProps { emailPrimary: Maybe; emailSecondary: Maybe; + phonePrimary: Maybe; phoneSecondary: Maybe; + mobilePrimary: Maybe; mobileSecondary: Maybe; + fax: Maybe; website: Maybe; legalRecord: Maybe; + defaultTaxes: Collection; languageCode: LanguageCode; diff --git a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts index 2765d950..9e4465ef 100644 --- a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts @@ -171,7 +171,7 @@ export class CustomerDomainMapper // source.default_taxes is stored as a comma-separated string const defaultTaxes = new Collection(); if (!isNullishOrEmpty(source.default_taxes)) { - source.default_taxes.split(",").map((taxCode, index) => { + source.default_taxes!.split(",").map((taxCode, index) => { const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); if (tax) { defaultTaxes.add(tax!); diff --git a/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts index 0dfd77d6..42e8137d 100644 --- a/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts @@ -1,4 +1,11 @@ -import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Sequelize, +} from "sequelize"; export type CustomerCreationAttributes = InferCreationAttributes & {}; @@ -13,43 +20,43 @@ export class CustomerModel extends Model< declare id: string; declare company_id: string; - declare reference: string; + declare reference: CreationOptional; declare is_company: boolean; declare name: string; - declare trade_name: string; - declare tin: string; + declare trade_name: CreationOptional; + declare tin: CreationOptional; - declare street: string; - declare street2: string; - declare city: string; - declare province: string; - declare postal_code: string; - declare country: string; + declare street: CreationOptional; + declare street2: CreationOptional; + declare city: CreationOptional; + declare province: CreationOptional; + declare postal_code: CreationOptional; + declare country: CreationOptional; // Correos electrónicos - declare email_primary: string; - declare email_secondary: string; + declare email_primary: CreationOptional; + declare email_secondary: CreationOptional; // Teléfonos fijos - declare phone_primary: string; - declare phone_secondary: string; + declare phone_primary: CreationOptional; + declare phone_secondary: CreationOptional; // Móviles - declare mobile_primary: string; - declare mobile_secondary: string; + declare mobile_primary: CreationOptional; + declare mobile_secondary: CreationOptional; - declare fax: string; - declare website: string; + declare fax: CreationOptional; + declare website: CreationOptional; - declare legal_record: string; + declare legal_record: CreationOptional; - declare default_taxes: string; + declare default_taxes: CreationOptional; declare status: string; - declare language_code: string; - declare currency_code: string; + declare language_code: CreationOptional; + declare currency_code: CreationOptional; - declare factuges_id: string; + declare factuges_id: CreationOptional; static associate(database: Sequelize) {} @@ -187,6 +194,12 @@ export default (database: Sequelize) => { allowNull: true, }, + status: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "active", + }, + language_code: { type: DataTypes.STRING(2), allowNull: false, @@ -199,12 +212,6 @@ export default (database: Sequelize) => { defaultValue: "EUR", }, - status: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: "active", - }, - factuges_id: { type: DataTypes.STRING, allowNull: true, diff --git a/packages/rdx-ddd/src/helpers/normalizers.ts b/packages/rdx-ddd/src/helpers/normalizers.ts index c0482ecd..bbd16167 100644 --- a/packages/rdx-ddd/src/helpers/normalizers.ts +++ b/packages/rdx-ddd/src/helpers/normalizers.ts @@ -21,7 +21,12 @@ export function maybeFromNullableString(input?: string | null): Maybe { } /** Maybe -> null para transporte */ -export function toNullable(m: Maybe, map?: (t: T) => any): any | null { +export function toNullable(m: Maybe, map: (t: T) => R): R | null { + if (!m || m.isNone()) return null; + return map(m.unwrap() as T); +} + +export function toNullable2(m: Maybe, map?: (t: T) => unknown): unknown | null { if (!m || m.isNone()) return null; const v = m.unwrap() as T; return map ? String(map(v)) : String(v); diff --git a/packages/rdx-utils/src/helpers/collection.ts b/packages/rdx-utils/src/helpers/collection.ts index aa352bc0..35f3fec8 100644 --- a/packages/rdx-utils/src/helpers/collection.ts +++ b/packages/rdx-utils/src/helpers/collection.ts @@ -3,8 +3,8 @@ * Ofrece métodos básicos para manipular, consultar y recorrer los elementos. */ export class Collection { - private items: T[]; - private totalItems: number; + protected items: T[]; + protected totalItems: number; /** * Crea una nueva colección. @@ -46,7 +46,7 @@ export class Collection { return this.removeByIndex(index); } - public removeByIndex(index: number) { + removeByIndex(index: number) { if (index !== -1) { this.items.splice(index, 1); if (this.totalItems !== null) {