diff --git a/docs/customer-invoices/invoice-series.md b/docs/customer-invoices/invoice-series.md index 29b090ef..af710984 100644 --- a/docs/customer-invoices/invoice-series.md +++ b/docs/customer-invoices/invoice-series.md @@ -2,6 +2,8 @@ > Documento mantenido solo como referencia histórica previa a la retirada de `/catalogs/invoice-series`. > Estado actual: la API canónica es `/document-series` y la tabla canónica es `document_series`. +> Las tablas `customer_invoices`, `customer_invoice_items` y `customer_invoice_taxes` +> ya no forman parte del runtime actual. ## Decisión consolidada diff --git a/docs/customer-invoices/sql/README.md b/docs/customer-invoices/sql/README.md index fceb0c01..f522c464 100644 --- a/docs/customer-invoices/sql/README.md +++ b/docs/customer-invoices/sql/README.md @@ -4,6 +4,10 @@ Este directorio mezcla SQL vigentes de soporte con SQL historicos/transicionales. +Las tablas legacy `customer_invoices`, `customer_invoice_items` y +`customer_invoice_taxes` no forman parte del runtime actual. Solo pueden aparecer +en SQL historico de migracion o validacion. + ## SQL vigentes - `add-proforma-document-series-columns.sql` diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts index 4511d743..669a9c19 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts @@ -40,7 +40,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps return { id: invoice.id.toString(), company_id: invoice.companyId.toString(), - is_proforma: false, document_series_id: maybeToNullable(invoice.documentSeriesId, (value) => value), invoice_series_code: maybeToNullable(Maybe.some(invoice.series), (value) => value.toString()), diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts index 6a7d0264..87e74c19 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts @@ -17,7 +17,6 @@ import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verif export interface IIssuedInvoiceFullSnapshot { id: string; company_id: string; - is_proforma: boolean; document_series_id: string | null; invoice_series_code: string | null; diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts index ae4ecabb..8528fc52 100644 --- a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts @@ -11,8 +11,8 @@ export enum INVOICE_STATUS { APPROVED = "approved", // <- Proforma REJECTED = "rejected", // <- Proforma - // status === "issued" <- (si is_proforma === true) => Es una proforma (histórica) - // status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu + // `issued` se conserva como estado final compartido tras el split histórico + // entre proformas e issued invoices. ISSUED = "issued", } diff --git a/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts b/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts deleted file mode 100644 index 3c169d57..00000000 --- a/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CompositeSpecification } from "@repo/rdx-ddd"; - -import type { CustomerInvoice } from "../aggregates"; - -export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification { - public async isSatisfiedBy(proforma: CustomerInvoice): Promise { - return proforma.isProforma; - } -} diff --git a/modules/customer-invoices/src/api/domain/specs/index.ts b/modules/customer-invoices/src/api/domain/specs/index.ts index 550b9ec5..6ec4d83d 100644 --- a/modules/customer-invoices/src/api/domain/specs/index.ts +++ b/modules/customer-invoices/src/api/domain/specs/index.ts @@ -1,3 +1,2 @@ -export * from "./customer-invoice-is-proforma.specification"; export * from "./proforma-can-transtion-to-issued.specification"; export * from "./status-invoice-is-draft.specification"; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts index 09a823dd..69dbc515 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts @@ -1,6 +1,3 @@ -import customerInvoiceModelInit from "./models/customer-invoice.model"; -import customerInvoiceItemModelInit from "./models/customer-invoice-item.model"; -import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model"; import issuedInvoiceModelInit from "./models/sequelize-issued-invoice.model"; import issuedInvoiceItemModelInit from "./models/sequelize-issued-invoice-item.model"; import issuedInvoiceTaxModelInit from "./models/sequelize-issued-invoice-tax.model"; @@ -13,9 +10,6 @@ export * from "./models"; // Array de inicializadores para que registerModels() lo use. export const models = [ - customerInvoiceModelInit, - customerInvoiceItemModelInit, - customerInvoiceTaxesModelInit, proformaModelInit, proformaItemModelInit, proformaTaxModelInit, diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-item.model.ts deleted file mode 100644 index dfc72cb0..00000000 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-item.model.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { - type CreationOptional, - DataTypes, - type InferAttributes, - type InferCreationAttributes, - Model, - type NonAttribute, - type Sequelize, -} from "sequelize"; - -import type { CustomerInvoiceModel } from "./customer-invoice.model"; - -export type CustomerInvoiceItemCreationAttributes = InferCreationAttributes< - CustomerInvoiceItemModel, - { omit: "invoice" } ->; - -export class CustomerInvoiceItemModel extends Model< - InferAttributes, - InferCreationAttributes -> { - declare item_id: string; - declare invoice_id: string; - - declare position: number; - - declare description: CreationOptional; - - declare quantity_value: CreationOptional; - declare quantity_scale: number; - - declare unit_amount_value: CreationOptional; - declare unit_amount_scale: number; - - // Subtotal (cantidad * importe unitario) - declare subtotal_amount_value: number; - declare subtotal_amount_scale: number; - - // Discount percentage - declare item_discount_percentage_value: CreationOptional; - declare item_discount_percentage_scale: number; - - // Discount amount - declare item_discount_amount_value: number; - declare item_discount_amount_scale: number; - - // Porcentaje de descuento global proporcional a esta línea. - declare global_discount_percentage_value: number; - declare global_discount_percentage_scale: number; - - // Importe del descuento global para esta línea - declare global_discount_amount_value: number; - declare global_discount_amount_scale: number; - - // Suma de los dos descuentos: el de la linea + el global - declare total_discount_amount_value: number; - declare total_discount_amount_scale: number; - - // Taxable amount (base imponible tras los dos descuentos) - declare taxable_amount_value: number; - declare taxable_amount_scale: number; - - // IVA percentage - declare iva_code: CreationOptional; - - declare iva_percentage_value: CreationOptional; - declare iva_percentage_scale: number; - - // IVA amount - declare iva_amount_value: number; - 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: number; - declare rec_amount_scale: number; - - // Retention percentage - declare retention_code: CreationOptional; - - declare retention_percentage_value: CreationOptional; - declare retention_percentage_scale: number; - - declare retention_amount_value: number; - declare retention_amount_scale: number; - - // Total taxes amount / taxes total - declare taxes_amount_value: number; - declare taxes_amount_scale: number; - - // Total - declare total_amount_value: number; - declare total_amount_scale: number; - - // Relaciones - declare invoice: NonAttribute; - - static associate(database: Sequelize) { - const models = database.models; - const requiredModels = ["CustomerInvoiceModel", "CustomerInvoiceItemModel"]; - - // Comprobamos que los modelos existan - for (const name of requiredModels) { - if (!models[name]) { - throw new Error(`[CustomerInvoiceItemModel.associate] Missing model: ${name}`); - } - } - - const { CustomerInvoiceModel, CustomerInvoiceItemModel } = models; - - CustomerInvoiceItemModel.belongsTo(CustomerInvoiceModel, { - as: "invoice", - targetKey: "id", - foreignKey: "invoice_id", - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); - } -} - -export default (database: Sequelize) => { - CustomerInvoiceItemModel.init( - { - item_id: { - type: DataTypes.UUID, - primaryKey: true, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - invoice_id: { - type: DataTypes.UUID, - allowNull: false, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - position: { - type: new DataTypes.MEDIUMINT().UNSIGNED, - autoIncrement: false, - allowNull: false, - }, - - description: { - type: new DataTypes.TEXT(), - allowNull: true, - defaultValue: null, - }, - - quantity_value: { - type: new DataTypes.BIGINT(), - allowNull: true, - defaultValue: null, - }, - - quantity_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - unit_amount_value: { - type: new DataTypes.BIGINT(), - allowNull: true, - defaultValue: null, - }, - - unit_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - subtotal_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - subtotal_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - item_discount_percentage_value: { - type: new DataTypes.SMALLINT(), - allowNull: true, - defaultValue: null, - }, - - item_discount_percentage_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - item_discount_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - item_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - global_discount_percentage_value: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 0, - }, - - global_discount_percentage_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - global_discount_amount_value: { - type: new DataTypes.BIGINT(), - allowNull: false, - defaultValue: 0, - }, - - global_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - total_discount_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - total_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - taxable_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - taxable_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - // IVA % - - iva_code: { - type: DataTypes.STRING(40), - allowNull: true, - defaultValue: null, - }, - iva_percentage_value: { - type: DataTypes.SMALLINT, - allowNull: true, - defaultValue: null, - }, - iva_percentage_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 2, - }, - - iva_amount_value: { - type: DataTypes.BIGINT, - allowNull: false, - defaultValue: 0, - }, - 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: false, - defaultValue: 0, - }, - 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: false, - defaultValue: 0, - }, - retention_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - taxes_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - taxes_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - - total_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - total_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - }, - { - sequelize: database, - modelName: "CustomerInvoiceItemModel", - tableName: "customer_invoice_items", - - charset: "utf8mb4", - collate: "utf8mb4_unicode_ci", - underscored: true, - - indexes: [{ fields: ["invoice_id"] }, { fields: ["invoice_id", "position"] }], - - whereMergeStrategy: "and", // <- cómo tratar el merge de un scope - - defaultScope: { - order: [["position", "ASC"]], - }, - - scopes: {}, - } - ); - - return CustomerInvoiceItemModel; -}; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts deleted file mode 100644 index e2cca91f..00000000 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - type CreationOptional, - DataTypes, - type InferAttributes, - type InferCreationAttributes, - Model, - type NonAttribute, - type Sequelize, -} from "sequelize"; - -import type { Proforma } from "../../../../../domain"; - -export type CustomerInvoiceTaxCreationAttributes = InferCreationAttributes< - CustomerInvoiceTaxModel, - { omit: "invoice" } ->; - -export class CustomerInvoiceTaxModel extends Model< - InferAttributes, - InferCreationAttributes -> { - declare tax_id: string; - declare invoice_id: string; - - // Taxable amount (base imponible) - declare taxable_amount_value: number; - 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: number; - 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: number; - 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: number; - declare retention_amount_scale: number; - - // Total taxes amount / taxes total - declare taxes_amount_value: number; - declare taxes_amount_scale: number; - - // Relaciones - declare invoice: NonAttribute; - - static associate(database: Sequelize) { - const models = database.models; - const requiredModels = ["CustomerInvoiceModel"]; - - // Comprobamos que los modelos existan - for (const name of requiredModels) { - if (!models[name]) { - throw new Error(`[CustomerInvoiceTaxModel.associate] Missing model: ${name}`); - } - } - - const { CustomerInvoiceModel } = database.models; - - CustomerInvoiceTaxModel.belongsTo(CustomerInvoiceModel, { - as: "invoice", - foreignKey: "invoice_id", - targetKey: "id", - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); - } - - static hooks(_database: Sequelize) { - // - } -} - -export default (database: Sequelize) => { - CustomerInvoiceTaxModel.init( - { - tax_id: { - type: DataTypes.UUID, - primaryKey: true, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - invoice_id: { - type: DataTypes.UUID, - allowNull: false, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - 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, - }, - - // IVA % - - iva_code: { - type: DataTypes.STRING(40), - allowNull: true, - defaultValue: null, - }, - - iva_percentage_value: { - type: DataTypes.SMALLINT, - allowNull: true, - defaultValue: null, - }, - iva_percentage_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 2, - }, - - iva_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - iva_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - // REC % - rec_code: { - type: DataTypes.STRING(40), - allowNull: true, - defaultValue: null, - }, - - rec_percentage_value: { - type: DataTypes.SMALLINT, - allowNull: true, - defaultValue: null, - }, - rec_percentage_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 2, - }, - - rec_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - rec_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - // Retención % - retention_code: { - type: DataTypes.STRING(40), - allowNull: true, - defaultValue: null, - }, - - retention_percentage_value: { - type: DataTypes.SMALLINT, - allowNull: true, - defaultValue: null, - }, - retention_percentage_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 2, - }, - - retention_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - retention_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - taxes_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - taxes_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 4, - }, - }, - { - sequelize: database, - modelName: "CustomerInvoiceTaxModel", - tableName: "customer_invoice_taxes", - - charset: "utf8mb4", - collate: "utf8mb4_unicode_ci", - underscored: true, - - indexes: [ - { - name: "invoice_id_idx", - fields: ["invoice_id"], - }, - { - name: "invoice_iva_code_unique", - fields: ["invoice_id", "iva_code"], - unique: true, // cada impuesto aparece como máximo una vez - }, - ], - - whereMergeStrategy: "and", // <- cómo tratar el merge de un scope - - defaultScope: {}, - - scopes: {}, - } - ); - - return CustomerInvoiceTaxModel; -}; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts deleted file mode 100644 index 77606305..00000000 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts +++ /dev/null @@ -1,580 +0,0 @@ -import type { CustomerModel } from "@erp/customers/api"; -import { - type CreationOptional, - DataTypes, - type InferAttributes, - type InferCreationAttributes, - Model, - type NonAttribute, - type Sequelize, -} from "sequelize"; - -import type { - CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItemModel, -} from "./customer-invoice-item.model"; -import type { - CustomerInvoiceTaxCreationAttributes, - CustomerInvoiceTaxModel, -} from "./customer-invoice-tax.model"; -import type { - VerifactuRecordCreationAttributes, - VerifactuRecordModel, -} from "./verifactu-record.model"; - -export type CustomerInvoiceCreationAttributes = InferCreationAttributes< - CustomerInvoiceModel, - { omit: "items" | "taxes" | "current_customer" | "verifactu" } -> & { - items?: CustomerInvoiceItemCreationAttributes[]; - taxes?: CustomerInvoiceTaxCreationAttributes[]; - verifactu?: VerifactuRecordCreationAttributes; -}; - -export class CustomerInvoiceModel extends Model< - InferAttributes, - InferCreationAttributes -> { - // Version - // declare version: CreationOptional; - - declare id: string; - declare company_id: string; - - // status === issued <- (si is_proforma === true) => Es una proforma (histórica) - // status === issued <- (si is_proforma === false) => Factura y enviada a Veri*Factu - declare is_proforma: boolean; - declare status: string; - - declare proforma_id: CreationOptional; // ID de la proforma origen de la factura - - declare series: CreationOptional; - declare invoice_number: CreationOptional; - declare invoice_date: CreationOptional; - declare operation_date: CreationOptional; - declare language_code: CreationOptional; - declare currency_code: CreationOptional; - - declare reference: CreationOptional; - declare description: CreationOptional; - - declare notes: CreationOptional; - - // Método de pago - declare payment_method_id: CreationOptional; - declare payment_method_description: CreationOptional; - - // Tax regime - declare tax_regime_code: CreationOptional; - declare tax_regime_description: CreationOptional; - - // Subtotal - declare subtotal_amount_value: number; - declare subtotal_amount_scale: number; - - // Items discount amount (suma de descuentos individuales por ítem) - declare items_discount_amount_value: number; - declare items_discount_amount_scale: number; - - // Global/header discount percentage - declare global_discount_percentage_value: number; - declare global_discount_percentage_scale: number; - - // Global/header discount amount - declare global_discount_amount_value: number; - declare global_discount_amount_scale: number; - - // Total discount amount (subtotal - descuentos) - declare total_discount_amount_value: number; - declare total_discount_amount_scale: number; - - // Taxable amount (base imponible) - declare taxable_amount_value: number; - declare taxable_amount_scale: number; - - // IVA amount - declare iva_amount_value: number; - declare iva_amount_scale: number; - - // Recargo de equivalencia amount - declare rec_amount_value: number; - declare rec_amount_scale: number; - - // Retention amount - declare retention_amount_value: number; - declare retention_amount_scale: number; - - // Total taxes amount / taxes total - declare taxes_amount_value: number; - declare taxes_amount_scale: number; - - // Total - declare total_amount_value: number; - declare total_amount_scale: number; - - // Customer - declare customer_id: 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; - declare taxes: NonAttribute; - declare current_customer?: NonAttribute; - declare verifactu?: NonAttribute; - declare proforma?: NonAttribute; - declare linked_invoice?: NonAttribute; - - static associate(database: Sequelize) { - const models = database.models; - const requiredModels = [ - "CustomerInvoiceModel", - "CustomerInvoiceItemModel", - "CustomerModel", - "CustomerInvoiceTaxModel", - "VerifactuRecordModel", - ]; - - // Comprobamos que los modelos existan - for (const name of requiredModels) { - if (!models[name]) { - throw new Error(`[CustomerInvoiceModel.associate] Missing model: ${name}`); - } - } - - // Los modelos existen - const { - CustomerInvoiceModel, - CustomerInvoiceItemModel, - CustomerModel, - CustomerInvoiceTaxModel, - VerifactuRecordModel, - } = models; - - /*CustomerInvoiceModel.hasOne(VerifactuRecordModel, { - as: "verifactu", - foreignKey: "invoice_id", - });*/ - - CustomerInvoiceModel.belongsTo(CustomerModel, { - as: "current_customer", - foreignKey: "customer_id", - constraints: false, - }); - - CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, { - as: "items", - foreignKey: "invoice_id", - sourceKey: "id", - constraints: true, - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); - - CustomerInvoiceModel.hasMany(CustomerInvoiceTaxModel, { - as: "taxes", - foreignKey: "invoice_id", - sourceKey: "id", - constraints: true, - onDelete: "CASCADE", - onUpdate: "CASCADE", - }); - - // Relaciones con proforma e invoice vinculada (si esta factura es una proforma o tiene una proforma origen) - - CustomerInvoiceModel.belongsTo(CustomerInvoiceModel, { - as: "proforma", - foreignKey: "proforma_id", - targetKey: "id", - constraints: true, - onDelete: "SET NULL", - onUpdate: "CASCADE", - }); - - CustomerInvoiceModel.hasOne(CustomerInvoiceModel, { - as: "linked_invoice", - foreignKey: "proforma_id", - sourceKey: "id", - constraints: true, - onDelete: "SET NULL", - onUpdate: "CASCADE", - }); - } - - static hooks(_database: Sequelize) { - // - } -} - -export default (database: Sequelize) => { - CustomerInvoiceModel.init( - { - /*version: { - type: DataTypes.INTEGER.UNSIGNED, - allowNull: false, - defaultValue: 0, - },*/ - - id: { - type: DataTypes.UUID, - primaryKey: true, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - company_id: { - type: DataTypes.UUID, - allowNull: false, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - is_proforma: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - - proforma_id: { - type: DataTypes.UUID, - allowNull: true, - defaultValue: null, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - status: { - type: new DataTypes.STRING(), - allowNull: false, - defaultValue: "draft", - }, - - series: { - type: new DataTypes.STRING(10), - allowNull: true, - defaultValue: null, - }, - - invoice_number: { - type: new DataTypes.STRING(12), - allowNull: true, - defaultValue: null, - }, - - invoice_date: { - type: new DataTypes.DATEONLY(), - allowNull: false, - }, - - operation_date: { - type: new DataTypes.DATEONLY(), - allowNull: true, - defaultValue: null, - }, - - language_code: { - type: DataTypes.STRING(2), - allowNull: false, - defaultValue: "es", - }, - - currency_code: { - type: new DataTypes.STRING(3), - allowNull: false, - defaultValue: "EUR", - }, - - reference: { - type: new DataTypes.STRING(), - allowNull: true, - defaultValue: null, - }, - - description: { - type: new DataTypes.STRING(), - allowNull: true, - defaultValue: null, - }, - - notes: { - type: new DataTypes.TEXT(), - allowNull: true, - defaultValue: null, - }, - - payment_method_id: { - type: DataTypes.UUID, - allowNull: true, - defaultValue: null, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - payment_method_description: { - type: new DataTypes.STRING(), - allowNull: true, - defaultValue: null, - }, - - tax_regime_code: { - type: DataTypes.STRING(2), - allowNull: true, - }, - - tax_regime_description: { - type: new DataTypes.STRING(), - allowNull: true, - }, - - subtotal_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - subtotal_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - items_discount_amount_value: { - type: new DataTypes.BIGINT(), - allowNull: false, - defaultValue: 0, - }, - - items_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - global_discount_percentage_value: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 0, - }, - - global_discount_percentage_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - global_discount_amount_value: { - type: new DataTypes.BIGINT(), - allowNull: false, - defaultValue: 0, - }, - - global_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - total_discount_amount_value: { - type: new DataTypes.BIGINT(), - allowNull: false, - defaultValue: 0, - }, - - total_discount_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - 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: 2, - }, - - iva_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - - iva_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - rec_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - rec_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - retention_amount_value: { - type: DataTypes.BIGINT, - allowNull: true, - defaultValue: null, - }, - retention_amount_scale: { - type: DataTypes.SMALLINT, - allowNull: false, - defaultValue: 4, - }, - - taxes_amount_value: { - 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: 2, - }, - - total_amount_value: { - type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes - allowNull: false, - defaultValue: 0, - }, - - total_amount_scale: { - type: new DataTypes.SMALLINT(), - allowNull: false, - defaultValue: 2, - }, - - customer_id: { - type: DataTypes.UUID, - allowNull: false, - charset: "utf8mb4", - collate: "utf8mb4_bin", - } as any, - - customer_tin: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_name: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_street: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_street2: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_city: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_province: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_postal_code: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - - customer_country: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - }, - { - sequelize: database, - modelName: "CustomerInvoiceModel", - tableName: "customer_invoices", - - charset: "utf8mb4", - collate: "utf8mb4_unicode_ci", - underscored: true, - paranoid: true, // softs deletes - timestamps: true, - - createdAt: "created_at", - updatedAt: "updated_at", - deletedAt: "deleted_at", - - indexes: [ - { - name: "idx_invoices_company_date", - fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }], - }, - - { - name: "idx_invoice_company_series_number", - fields: ["company_id", "series", "invoice_number", "is_proforma"], - unique: true, - }, // <- para consulta get - - { name: "idx_invoice_date", fields: ["invoice_date"] }, // <- para ordenación - - { name: "idx_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get - - { name: "uq_invoice_proforma_id", fields: ["proforma_id"], unique: true }, // <- para asegurar que una proforma solo tenga una factura vinculada - - // Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter" - /*{ - name: "ft_customer_invoice", - type: "FULLTEXT", - fields: [ - "CustomerInvoiceModel.invoice_number", - "CustomerInvoiceModel.reference", - "CustomerInvoiceModel.description", - ], - },*/ - ], - - whereMergeStrategy: "and", // <- cómo tratar el merge de un scope - - defaultScope: {}, - - scopes: {}, - - /*hooks: { - // Incrementa la versión en cada update exitoso (OCC). - beforeUpdate: (instance) => { - const current = instance.get("version") as number; - instance.set("version", current + 1); - }, - },*/ - } - ); - - return CustomerInvoiceModel; -}; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts index bcd385f5..14253ace 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts @@ -1,6 +1,3 @@ -export * from "./customer-invoice.model"; -export * from "./customer-invoice-item.model"; -export * from "./customer-invoice-tax.model"; export * from "./sequelize-proforma.model"; export * from "./sequelize-proforma-item.model"; export * from "./sequelize-proforma-tax.model"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts index 1cd9e029..9fc742e8 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts @@ -1,7 +1,5 @@ export * from "./issued-invoice-persistence.di"; export * from "./issued-invoice-persistence-mappers-v2.di"; export * from "./issued-invoice-public-services"; -//export * from "./issued-invoice-persistence-mappers.di"; export * from "./issued-invoice-repositories-v2.di"; -//export * from "./issued-invoice-repositories.di"; export * from "./issued-invoices.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence-mappers.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence-mappers.di.ts deleted file mode 100644 index d3a2cae4..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence-mappers.di.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ICatalogs } from "@erp/core/api"; - -import { - SequelizeIssuedInvoiceDomainMapper, - SequelizeIssuedInvoiceSummaryMapper, -} from "../persistence"; - -export interface IIssuedInvoicePersistenceMappers { - domainMapper: SequelizeIssuedInvoiceDomainMapper; - summaryMapper: SequelizeIssuedInvoiceSummaryMapper; -} - -export const buildLegacyIssuedInvoicePersistenceMappers = ( - catalogs: ICatalogs -): IIssuedInvoicePersistenceMappers => { - const { taxCatalog } = catalogs; - - // Mappers para el repositorio - const domainMapper = new SequelizeIssuedInvoiceDomainMapper({ - taxCatalog, - }); - const summaryMapper = new SequelizeIssuedInvoiceSummaryMapper(); - - return { - domainMapper, - summaryMapper, - }; -}; - -export const buildIssuedInvoicePersistenceMappers = buildLegacyIssuedInvoicePersistenceMappers; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence.di.ts index f2ff7f79..dbdfd544 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-persistence.di.ts @@ -12,33 +12,12 @@ type BuildIssuedInvoicePersistenceParams = { database: Sequelize; }; -/*export type LegacyIssuedInvoicePersistence = { - mode: "legacy"; - mappers: IIssuedInvoicePersistenceMappers; - repository: ReturnType; -};*/ - export type V2IssuedInvoicePersistence = { mode: "v2"; mappers: IIssuedInvoicePersistenceMappersV2; repository: ReturnType; }; -/*export const buildLegacyIssuedInvoicePersistence = ( - params: BuildIssuedInvoicePersistenceParams -): LegacyIssuedInvoicePersistence => { - const mappers = buildLegacyIssuedInvoicePersistenceMappers(params.catalogs); - - return { - mode: "legacy", - mappers, - repository: buildLegacyIssuedInvoiceRepository({ - database: params.database, - mappers, - }), - }; -};*/ - export const buildV2IssuedInvoicePersistence = ( params: BuildIssuedInvoicePersistenceParams ): V2IssuedInvoicePersistence => { diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts deleted file mode 100644 index e3de293c..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Sequelize } from "sequelize"; - -import { IssuedInvoiceRepository } from "../persistence"; - -import type { IIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di"; - -export const buildLegacyIssuedInvoiceRepository = (params: { - database: Sequelize; - mappers: IIssuedInvoicePersistenceMappers; -}) => { - const { database, mappers } = params; - - return new IssuedInvoiceRepository(mappers.domainMapper, mappers.summaryMapper, database); -}; - -export const buildIssuedInvoiceRepository = buildLegacyIssuedInvoiceRepository; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts deleted file mode 100644 index ef7e55c2..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts +++ /dev/null @@ -1,553 +0,0 @@ -import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; -import { - CurrencyCode, - DomainValidationError, - LanguageCode, - TextValue, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, - maybeToNullable, -} from "@repo/rdx-ddd"; -import { Result, isNullishOrEmpty } from "@repo/rdx-utils"; - -import { - type InternalIssuedInvoiceProps, - InvoiceAmount, - InvoiceNumber, - InvoicePaymentMethod, - InvoiceSerie, - InvoiceStatus, - InvoiceTaxRegime, - IssuedInvoice, - IssuedInvoiceItems, - IssuedInvoiceTaxes, -} from "../../../../../../domain"; -import type { - CustomerInvoiceCreationAttributes, - CustomerInvoiceModel, -} from "../../../../../common"; - -import { SequelizeIssuedInvoiceItemDomainMapper } from "./sequelize-issued-invoice-item-domain.mapper"; -import { SequelizeIssuedInvoiceRecipientDomainMapper } from "./sequelize-issued-invoice-recipient-domain.mapper"; -import { SequelizeIssuedInvoiceTaxesDomainMapper } from "./sequelize-issued-invoice-taxes-domain.mapper"; -import { SequelizeIssuedInvoiceVerifactuDomainMapper } from "./sequelize-verifactu-record-domain.mapper"; - -export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceModel, - CustomerInvoiceCreationAttributes, - IssuedInvoice -> { - private _itemsMapper: SequelizeIssuedInvoiceItemDomainMapper; - private _recipientMapper: SequelizeIssuedInvoiceRecipientDomainMapper; - private _taxesMapper: SequelizeIssuedInvoiceTaxesDomainMapper; - private _verifactuMapper: SequelizeIssuedInvoiceVerifactuDomainMapper; - - constructor(params: MapperParamsType) { - super(); - - this._itemsMapper = new SequelizeIssuedInvoiceItemDomainMapper(params); // Instanciar el mapper de items - this._recipientMapper = new SequelizeIssuedInvoiceRecipientDomainMapper(); - this._taxesMapper = new SequelizeIssuedInvoiceTaxesDomainMapper(params); - this._verifactuMapper = new SequelizeIssuedInvoiceVerifactuDomainMapper(); - } - - private _mapAttributesToDomain(raw: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); - - const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); - - const linkedProformaId = extractOrPushError( - UniqueID.create(raw.proforma_id!), - "proforma_id", - errors - ); - - const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); - - // En el caso de "serie", al ser un campo opcional en BD - // pero obligatorio en el dominio, - // se le asignará un valor por defecto (cadena vacía) en caso de venir como nullish, para que así el VO lo valide y lance el error correspondiente. De esta forma evitamos que el mapper devuelva un null o undefined en el campo 'series' del dominio, lo cual podría generar errores difíciles de depurar posteriormente en el código. - const series = extractOrPushError(InvoiceSerie.create(raw.series ?? ""), "series", errors); - - const invoiceNumber = extractOrPushError( - InvoiceNumber.create(raw.invoice_number), - "invoice_number", - errors - ); - const invoiceReference = extractOrPushError( - Result.ok(`${String(raw.series ?? "")}${String(raw.invoice_number ?? "")}`), - "invoice_reference", - errors - ); - - // Fechas - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(raw.invoice_date), - "invoice_date", - errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableResult(raw.operation_date, (v) => UtcDate.createFromISO(v)), - "operation_date", - errors - ); - - // Idioma / divisa - const languageCode = extractOrPushError( - LanguageCode.create(raw.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(raw.currency_code), - "currency_code", - errors - ); - - // Textos opcionales - const reference = extractOrPushError( - maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), - "reference", - errors - ); - - // En el caso de "description", al ser un campo opcional en BD - // pero obligatorio en el dominio, - // se le asignará un valor por defecto (cadena vacía) en caso de venir como nullish, para que así el VO lo valide y lance el error correspondiente. De esta forma evitamos que el mapper devuelva un null o undefined en el campo 'series' del dominio, lo cual podría generar errores difíciles de depurar posteriormente en el código. - const description = extractOrPushError( - Result.ok(String(raw.description ?? "")), - "description", - errors - ); - - const notes = extractOrPushError( - maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)), - "notes", - errors - ); - - // Método de pago (VO opcional con id + descripción) - let paymentMethod: InvoicePaymentMethod | undefined; - - if (!isNullishOrEmpty(raw.payment_method_id)) { - const paymentId = extractOrPushError( - UniqueID.create(String(raw.payment_method_id)), - "paymentMethod.id", - errors - ); - - const paymentVO = extractOrPushError( - InvoicePaymentMethod.create( - { name: String(raw.payment_method_description ?? "") }, - paymentId ?? undefined - ), - "payment_method_description", - errors - ); - - if (paymentVO) { - paymentMethod = paymentVO; - } - } - - const taxRegime = extractOrPushError( - !isNullishOrEmpty(raw.tax_regime_code) - ? InvoiceTaxRegime.create({ - code: String(raw.tax_regime_code), - description: String(raw.tax_regime_description ?? raw.tax_regime_code), - }) - : Result.fail( - new DomainValidationError( - "MISSING_TAX_REGIME", - "tax_regime", - "Issued invoice requires persisted tax regime" - ) - ), - "tax_regime", - errors - ); - - const subtotalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.subtotal_amount_value, - currency_code: currencyCode?.code, - }), - "subtotal_amount_value", - errors - ); - - // Total descuento de líneas - - const itemsDiscountAmount = extractOrPushError( - InvoiceAmount.create({ - value: Number(raw.items_discount_amount_value ?? 0), - currency_code: currencyCode?.code, - }), - "items_discount_amount_value", - errors - ); - - // % descuento global (VO) - const globalDiscountPercentage = extractOrPushError( - DiscountPercentage.create({ - value: Number(raw.global_discount_percentage_value ?? 0), - }), - "global_discount_percentage_value", - errors - ); - - const globalDiscountAmount = extractOrPushError( - InvoiceAmount.create({ - value: Number(raw.global_discount_amount_value ?? 0), - currency_code: currencyCode?.code, - }), - "global_discount_amount_value", - errors - ); - - const totalDiscountAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_discount_amount_value, - currency_code: currencyCode?.code, - }), - "total_discount_amount_value", - errors - ); - - const taxableAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxable_amount_value, - currency_code: currencyCode?.code, - }), - "taxable_amount_value", - errors - ); - - const ivaAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.iva_amount_value, - currency_code: currencyCode?.code, - }), - "iva_amount_value", - errors - ); - - const recAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.rec_amount_value, - currency_code: currencyCode?.code, - }), - "rec_amount_value", - errors - ); - - const retentionAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.retention_amount_value, - currency_code: currencyCode?.code, - }), - "retention_amount_value", - errors - ); - - const taxesAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxes_amount_value, - currency_code: currencyCode?.code, - }), - "taxes_amount_value", - errors - ); - - const totalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_amount_value, - currency_code: currencyCode?.code, - }), - "total_amount_value", - errors - ); - - return { - invoiceId, - companyId, - customerId, - status, - series, - invoiceNumber, - invoiceReference, - invoiceDate, - operationDate, - reference, - description, - notes, - languageCode, - currencyCode, - paymentMethod, - taxRegime, - - subtotalAmount, - itemsDiscountAmount, - globalDiscountPercentage, - globalDiscountAmount, - totalDiscountAmount, - taxableAmount, - ivaAmount, - recAmount, - retentionAmount, - taxesAmount, - totalAmount, - - linkedProformaId, - }; - } - - public mapToDomain( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - try { - const errors: ValidationErrorDetail[] = []; - - // 1) Valores escalares (atributos generales) - const attributes = this._mapAttributesToDomain(raw, { errors, ...params }); - - // 2) Recipient (snapshot en la factura o include) - const recipientResult = this._recipientMapper.mapToDomain(raw, { - errors, - attributes, - ...params, - }); - - // 3) Verifactu (snapshot en la factura o include) - const verifactuResult = this._verifactuMapper.mapToDomain(raw.verifactu, { - errors, - attributes, - ...params, - }); - - // 4) Items (colección) - const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, { - errors, - attributes, - ...params, - }); - - // 5) Taxes (colección) - const taxesResults = this._taxesMapper.mapToDomainCollection(raw.taxes, raw.taxes.length, { - errors, - attributes, - ...params, - }); - - // 6) Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) - ); - } - - // 6) Construcción del agregado (Dominio) - - const verifactu = verifactuResult.data; - - const items = IssuedInvoiceItems.create({ - items: itemsResults.data.getAll(), - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - globalDiscountPercentage: attributes.globalDiscountPercentage!, - }); - - const taxes = IssuedInvoiceTaxes.create({ - taxes: taxesResults.data.getAll(), - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - }); - - const invoiceProps: InternalIssuedInvoiceProps = { - companyId: attributes.companyId!, - - status: attributes.status!, - series: attributes.series!, - invoiceNumber: attributes.invoiceNumber!, - invoiceReference: attributes.invoiceReference!, - invoiceDate: attributes.invoiceDate!, - operationDate: attributes.operationDate!, - - customerId: attributes.customerId!, - recipient: recipientResult.data, - - reference: attributes.reference!, - description: attributes.description!, - notes: attributes.notes!, - - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - - subtotalAmount: attributes.subtotalAmount!, - - itemsDiscountAmount: attributes.itemsDiscountAmount!, - globalDiscountPercentage: attributes.globalDiscountPercentage!, - globalDiscountAmount: attributes.globalDiscountAmount!, - totalDiscountAmount: attributes.totalDiscountAmount!, - - taxableAmount: attributes.taxableAmount!, - ivaAmount: attributes.ivaAmount!, - recAmount: attributes.recAmount!, - retentionAmount: attributes.retentionAmount!, - - taxesAmount: attributes.taxesAmount!, - totalAmount: attributes.totalAmount!, - - paymentMethod: attributes.paymentMethod!, - taxRegime: attributes.taxRegime!, - - taxes, - verifactu, - - linkedProformaId: attributes.linkedProformaId!, - }; - - const invoiceId = attributes.invoiceId!; - const invoice = IssuedInvoice.rehydrate(invoiceProps, items, invoiceId); - - return Result.ok(invoice); - } catch (err: unknown) { - return Result.fail(err as Error); - } - } - - public mapToPersistence( - source: IssuedInvoice, - params?: MapperParamsType - ): Result { - const errors: ValidationErrorDetail[] = []; - - // 1) Items - const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { - errors, - parent: source, - ...params, - }); - - if (itemsResult.isFailure) { - errors.push({ - path: "items", - message: itemsResult.error.message, - }); - } - - // 2) Taxes - const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, { - errors, - parent: source, - ...params, - }); - - if (taxesResult.isFailure) { - errors.push({ - path: "taxes", - message: taxesResult.error.message, - }); - } - - // 3) Cliente - const recipient = this._recipientMapper.mapToPersistence(source.recipient, { - errors, - parent: source, - ...params, - }); - - // 4) Verifactu - const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, { - errors, - parent: source, - ...params, - }); - - // 5) Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - console.error("Errors mapping issued invoice to persistence:", errors); - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) - ); - } - - const items = itemsResult.data; - const taxes = taxesResult.data; - const verifactu = verifactuResult.data; - - const invoiceValues: Partial = { - // Identificación - id: source.id.toPrimitive(), - company_id: source.companyId.toPrimitive(), - - // Flags / estado / serie / número - is_proforma: false, - status: source.status.toPrimitive(), - proforma_id: source.linkedProformaId.toPrimitive(), - - series: source.series.toPrimitive(), - invoice_number: source.invoiceNumber.toPrimitive(), - invoice_date: source.invoiceDate.toPrimitive(), - operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()), - language_code: source.languageCode.toPrimitive(), - currency_code: source.currencyCode.toPrimitive(), - - reference: maybeToNullable(source.reference, (reference) => reference), - description: source.description, - notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), - - payment_method_id: source.paymentMethod.toObjectString().id, - payment_method_description: source.paymentMethod.toObjectString().name, - tax_regime_code: source.taxRegime.code, - tax_regime_description: source.taxRegime.description, - - subtotal_amount_value: source.subtotalAmount.value, - subtotal_amount_scale: source.subtotalAmount.scale, - - items_discount_amount_value: source.itemsDiscountAmount.value, - items_discount_amount_scale: source.itemsDiscountAmount.scale, - - global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, - global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, - - global_discount_amount_value: source.globalDiscountAmount.value, - global_discount_amount_scale: source.globalDiscountAmount.scale, - - total_discount_amount_value: source.totalDiscountAmount.value, - total_discount_amount_scale: source.totalDiscountAmount.scale, - - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, - - total_amount_value: source.totalAmount.value, - total_amount_scale: source.totalAmount.scale, - - customer_id: source.customerId.toPrimitive(), - ...recipient, - - taxes, - items, - verifactu, - }; - - return Result.ok( - invoiceValues as CustomerInvoiceCreationAttributes - ); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts deleted file mode 100644 index 17248d7c..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts +++ /dev/null @@ -1,416 +0,0 @@ -import type { JsonTaxCatalogProvider } from "@erp/core"; -import { - DiscountPercentage, - type MapperParamsType, - SequelizeDomainMapper, - TaxPercentage, -} from "@erp/core/api"; -import { - UniqueID, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableOrEmptyString, - maybeFromNullableResult, - maybeToNullable, - maybeToNullableString, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import { - type IIssuedInvoiceCreateProps, - type IIssuedInvoiceItemCreateProps, - type IssuedInvoice, - IssuedInvoiceItem, - ItemAmount, - ItemDescription, - ItemQuantity, -} from "../../../../../../domain"; -import type { - CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItemModel, -} from "../../../../../common"; - -export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceItemModel, - CustomerInvoiceItemCreationAttributes, - IssuedInvoiceItem -> { - private readonly taxCatalog!: JsonTaxCatalogProvider; - - constructor(params: MapperParamsType) { - super(); - const { taxCatalog } = params as { - taxCatalog: JsonTaxCatalogProvider; - }; - - if (!taxCatalog) { - throw new Error('taxCatalog not defined ("SequelizeIssuedInvoiceItemDomainMapper")'); - } - - this.taxCatalog = taxCatalog; - } - - private mapAttributesToDomain( - raw: CustomerInvoiceItemModel, - params?: MapperParamsType - ): Partial & { itemId?: UniqueID } { - const { errors, index, attributes } = params as { - index: number; - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - const itemId = extractOrPushError( - UniqueID.create(raw.item_id), - `items[${index}].item_id`, - errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)), - `items[${index}].description`, - errors - ); - - const quantity = extractOrPushError( - maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })), - `items[${index}].quantity_value`, - errors - ); - - const unitAmount = extractOrPushError( - maybeFromNullableResult(raw.unit_amount_value, (value) => - ItemAmount.create({ value, currency_code: attributes.currencyCode?.code }) - ), - `items[${index}].unit_amount_value`, - errors - ); - - const subtotalAmount = extractOrPushError( - ItemAmount.create({ - value: raw.subtotal_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].subtotal_amount_value`, - errors - ); - - const itemDiscountPercentage = extractOrPushError( - maybeFromNullableResult(raw.item_discount_percentage_value, (v) => - DiscountPercentage.create({ value: v }) - ), - `items[${index}].item_discount_percentage_value`, - errors - ); - - const itemDiscountAmount = extractOrPushError( - ItemAmount.create({ - value: raw.item_discount_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].item_discount_amount_value`, - errors - ); - - const globalDiscountPercentage = extractOrPushError( - DiscountPercentage.create({ value: raw.global_discount_percentage_value }), - `items[${index}].global_discount_percentage_value`, - errors - ); - - const globalDiscountAmount = extractOrPushError( - ItemAmount.create({ - value: raw.global_discount_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].global_discount_amount_value`, - errors - ); - - const totalDiscountAmount = extractOrPushError( - ItemAmount.create({ - value: raw.total_discount_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].total_discount_amount_value`, - errors - ); - - const taxableAmount = extractOrPushError( - ItemAmount.create({ - value: raw.taxable_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].taxable_amount_value`, - errors - ); - - const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code); - - const ivaPercentage = extractOrPushError( - maybeFromNullableResult(raw.iva_percentage_value, (value) => TaxPercentage.create({ value })), - `items[${index}].iva_percentage_value`, - errors - ); - - const ivaAmount = extractOrPushError( - ItemAmount.create({ - value: raw.iva_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].iva_amount_value`, - errors - ); - - const recCode = maybeFromNullableOrEmptyString(raw.rec_code); - - const recPercentage = extractOrPushError( - maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })), - `items[${index}].rec_percentage_value`, - errors - ); - - const recAmount = extractOrPushError( - ItemAmount.create({ - value: raw.rec_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].rec_amount_value`, - errors - ); - - const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code); - - const retentionPercentage = extractOrPushError( - maybeFromNullableResult(raw.retention_percentage_value, (value) => - TaxPercentage.create({ value }) - ), - `items[${index}].retention_percentage_value`, - errors - ); - - const retentionAmount = extractOrPushError( - ItemAmount.create({ - value: raw.retention_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].retention_amount_value`, - errors - ); - - const taxesAmount = extractOrPushError( - ItemAmount.create({ - value: raw.taxes_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].taxes_amount_value`, - errors - ); - - const totalAmount = extractOrPushError( - ItemAmount.create({ - value: raw.total_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `items[${index}].total_amount_value`, - errors - ); - - return { - itemId, - languageCode: attributes.languageCode, - currencyCode: attributes.currencyCode, - description, - - quantity, - unitAmount, - subtotalAmount, - - itemDiscountPercentage, - itemDiscountAmount, - globalDiscountPercentage, - globalDiscountAmount, - totalDiscountAmount, - - taxableAmount, - - ivaCode, - ivaPercentage, - ivaAmount, - - recCode, - recPercentage, - recAmount, - - retentionCode, - retentionPercentage, - retentionAmount, - - taxesAmount, - totalAmount, - }; - } - - public mapToDomain( - source: CustomerInvoiceItemModel, - params?: MapperParamsType - ): Result { - const { errors, index } = params as { - index: number; - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - // 1) Valores escalares (atributos generales) - const attributes = this.mapAttributesToDomain(source, params); - - // Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) - ); - } - - // 2) Construcción del elemento de dominio - const itemId = attributes.itemId!; - const newItem = IssuedInvoiceItem.rehydrate( - { - description: attributes.description!, - - quantity: attributes.quantity!, - unitAmount: attributes.unitAmount!, - - subtotalAmount: attributes.subtotalAmount!, - - itemDiscountPercentage: attributes.itemDiscountPercentage!, - itemDiscountAmount: attributes.itemDiscountAmount!, - - globalDiscountPercentage: attributes.globalDiscountPercentage!, - globalDiscountAmount: attributes.globalDiscountAmount!, - - totalDiscountAmount: attributes.totalDiscountAmount!, - - taxableAmount: attributes.taxableAmount!, - - ivaCode: attributes.ivaCode!, - ivaPercentage: attributes.ivaPercentage!, - ivaAmount: attributes.ivaAmount!, - - recCode: attributes.recCode!, - recPercentage: attributes.recPercentage!, - recAmount: attributes.recAmount!, - - retentionCode: attributes.retentionCode!, - retentionPercentage: attributes.retentionPercentage!, - retentionAmount: attributes.retentionAmount!, - - taxesAmount: attributes.taxesAmount!, - totalAmount: attributes.totalAmount!, - - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - }, - itemId - ); - - return Result.ok(newItem); - } - - public mapToPersistence( - source: IssuedInvoiceItem, - params?: MapperParamsType - ): Result { - const { errors, index, parent } = params as { - index: number; - parent: IssuedInvoice; - errors: ValidationErrorDetail[]; - }; - - return Result.ok({ - item_id: source.id.toPrimitive(), - invoice_id: parent.id.toPrimitive(), - position: index, - - description: maybeToNullable(source.description, (v) => v.toPrimitive()), - - quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value), - quantity_scale: - maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ?? - ItemQuantity.DEFAULT_SCALE, - - unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value), - unit_amount_scale: - maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? - ItemAmount.DEFAULT_SCALE, - - subtotal_amount_value: source.subtotalAmount.toPrimitive().value, - subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale, - - item_discount_percentage_value: maybeToNullable( - source.itemDiscountPercentage, - (v) => v.toPrimitive().value - ), - item_discount_percentage_scale: - maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? - DiscountPercentage.DEFAULT_SCALE, - - item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value, - item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, - - global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, - global_discount_percentage_scale: - source.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE, - - global_discount_amount_value: source.globalDiscountAmount.value, - global_discount_amount_scale: source.globalDiscountAmount.scale, - - total_discount_amount_value: source.totalDiscountAmount.value, - total_discount_amount_scale: source.totalDiscountAmount.scale, - - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - // IVA - iva_code: maybeToNullableString(source.ivaCode), - - iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().value), - iva_percentage_scale: - maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().scale) ?? 2, - - iva_amount_value: source.ivaAmount.value, - iva_amount_scale: source.ivaAmount.scale, - - // REC - rec_code: maybeToNullableString(source.recCode), - - rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value), - rec_percentage_scale: - maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? 2, - - rec_amount_value: source.recAmount.value, - rec_amount_scale: source.recAmount.scale, - - // RET - retention_code: maybeToNullableString(source.retentionCode), - - retention_percentage_value: maybeToNullable( - source.retentionPercentage, - (v) => v.toPrimitive().value - ), - retention_percentage_scale: - maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? 2, - - retention_amount_value: source.retentionAmount.value, - retention_amount_scale: source.retentionAmount.scale, - - // - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, - - // - total_amount_value: source.totalAmount.value, - total_amount_scale: source.totalAmount.scale, - }); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts deleted file mode 100644 index 6cb8c36f..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { MapperParamsType } from "@erp/core/api"; -import { - City, - Country, - Name, - PostalCode, - Province, - Street, - TINNumber, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, - maybeToNullable, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import { type IIssuedInvoiceCreateProps, IssuedInvoiceRecipient } from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -export class SequelizeIssuedInvoiceRecipientDomainMapper { - public mapToDomain( - source: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - /** - * - Issued invoice -> snapshot de los datos (campos customer_*) - */ - - const { errors, attributes } = params as { - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - const _name = source.customer_name!; - const _tin = source.customer_tin!; - const _street = source.customer_street!; - const _street2 = source.customer_street2!; - const _city = source.customer_city!; - const _postal_code = source.customer_postal_code!; - const _province = source.customer_province!; - const _country = source.customer_country!; - - // Customer (snapshot) - const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); - - //const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); - const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors); - - const customerStreet = extractOrPushError(Street.create(_street), "customer_street", errors); - - const customerStreet2 = extractOrPushError( - maybeFromNullableResult(_street2, (value) => Street.create(value)), - "customer_street2", - errors - ); - - const customerCity = extractOrPushError(City.create(_city), "customer_city", errors); - - const customerProvince = extractOrPushError( - maybeFromNullableResult(_province, (value) => Province.create(value)), - "customer_province", - errors - ); - - const customerPostalCode = extractOrPushError( - PostalCode.create(_postal_code), - "customer_postal_code", - errors - ); - - const customerCountry = extractOrPushError( - maybeFromNullableResult(_country, (value) => Country.create(value)), - "customer_country", - errors - ); - - const createResult = IssuedInvoiceRecipient.create({ - name: customerName!, - tin: customerTin!, - street: customerStreet!, - street2: customerStreet2!, - city: customerCity!, - postalCode: customerPostalCode!, - province: customerProvince!, - country: customerCountry!, - }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice recipient entity creation failed", [ - { path: "recipient", message: createResult.error.message }, - ]) - ); - } - - return Result.ok(createResult.data); - } - - /** - * Mapea los datos del destinatario (recipient) de una factura de cliente - * al formato esperado por la capa de persistencia. - * - * Reglas: - * - Si la factura es proforma (`isProforma === true`), todos los campos de recipient son `null`. - * - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`. - * En caso contrario, se agrega un error de validación. - */ - mapToPersistence(source: IssuedInvoiceRecipient, params?: MapperParamsType) { - /*const { errors, parent } = params as { - parent: IssuedInvoice; - errors: ValidationErrorDetail[]; - };*/ - - const recipient = source; - - return { - customer_tin: recipient.tin.toString(), - customer_name: recipient.name.toPrimitive(), - customer_street: recipient.street.toPrimitive(), - customer_street2: maybeToNullable(recipient.street2, (v) => v.toPrimitive()), - customer_city: recipient.city.toPrimitive(), - customer_province: maybeToNullable(recipient.province, (v) => v.toPrimitive()), - customer_postal_code: recipient.postalCode.toPrimitive(), - customer_country: maybeToNullable(recipient.country, (v) => v.toPrimitive()), - }; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts deleted file mode 100644 index d35130d5..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { JsonTaxCatalogProvider } from "@erp/core"; -import { - DiscountPercentage, - type MapperParamsType, - SequelizeDomainMapper, - TaxPercentage, -} from "@erp/core/api"; -import { - Percentage, - UniqueID, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableOrEmptyString, - maybeFromNullableResult, - maybeToNullable, - maybeToNullableString, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import { - type IIssuedInvoiceCreateProps, - InvoiceAmount, - type IssuedInvoice, - IssuedInvoiceTax, - ItemAmount, -} from "../../../../../../domain"; -import type { - CustomerInvoiceTaxCreationAttributes, - CustomerInvoiceTaxModel, -} from "../../../../../common"; - -/** - * Mapper para customer_invoice_taxes - * - * Domina estructuras: - * { - * tax: Tax - * taxableAmount: ItemAmount - * taxesAmount: ItemAmount - * } - * - * Cada fila = un impuesto agregado en toda la factura. - */ -export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceTaxModel, - CustomerInvoiceTaxCreationAttributes, - IssuedInvoiceTax -> { - private taxCatalog!: JsonTaxCatalogProvider; - - constructor(params: MapperParamsType) { - super(); - const { taxCatalog } = params as { - taxCatalog: JsonTaxCatalogProvider; - }; - - if (!taxCatalog) { - throw new Error('taxCatalog not defined ("SequelizeIssuedInvoiceTaxesDomainMapper")'); - } - - this.taxCatalog = taxCatalog; - } - - public mapToDomain( - raw: CustomerInvoiceTaxModel, - params?: MapperParamsType - ): Result { - const { errors, index, attributes } = params as { - index: number; - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - const taxableAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxable_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `taxes[${index}].taxable_amount_value`, - errors - ); - - const ivaCode = raw.iva_code; - - // Una issued invoice debe traer IVA - const ivaPercentage = extractOrPushError( - TaxPercentage.create({ - value: Number(raw.iva_percentage_value), - }), - `taxes[${index}].iva_percentage_value`, - errors - ); - - const ivaAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.iva_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `taxes[${index}].iva_amount_value`, - errors - ); - - const recCode = maybeFromNullableOrEmptyString(raw.rec_code); - - const recPercentage = extractOrPushError( - maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })), - `taxes[${index}].rec_percentage_value`, - errors - ); - - const recAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.rec_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `taxes[${index}].rec_amount_value`, - errors - ); - - const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code); - - const retentionPercentage = extractOrPushError( - maybeFromNullableResult(raw.retention_percentage_value, (value) => - TaxPercentage.create({ value }) - ), - `taxes[${index}].retention_percentage_value`, - errors - ); - - const retentionAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.retention_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `taxes[${index}].retention_amount_value`, - errors - ); - - const taxesAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxes_amount_value, - currency_code: attributes.currencyCode?.code, - }), - `taxes[${index}].taxes_amount_value`, - errors - ); - - // Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice tax mapping failed [mapToDomain]", errors) - ); - } - - // 2) Construcción del elemento de dominio - const createResult = IssuedInvoiceTax.create({ - taxableAmount: taxableAmount!, - - ivaCode: ivaCode!, - ivaPercentage: ivaPercentage!, - ivaAmount: ivaAmount!, - - recCode: recCode, - recPercentage: recPercentage!, - recAmount: recAmount!, - - retentionCode: retentionCode, - retentionPercentage: retentionPercentage!, - retentionAmount: retentionAmount!, - - taxesAmount: taxesAmount!, - }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice tax group entity creation failed", [ - { path: `taxes[${index}]`, message: "Invoice tax group entity creation failed" }, - ]) - ); - } - - return createResult; - } - - public mapToPersistence( - source: IssuedInvoiceTax, - params?: MapperParamsType - ): Result { - const { errors, parent } = params as { - parent: IssuedInvoice; - errors: ValidationErrorDetail[]; - }; - - try { - const dto: CustomerInvoiceTaxCreationAttributes = { - tax_id: UniqueID.generateNewID().toPrimitive(), - invoice_id: parent.id.toPrimitive(), - - // TAXABLE AMOUNT - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - // IVA - iva_code: source.ivaCode, - - iva_percentage_value: source.ivaPercentage.value, - iva_percentage_scale: source.ivaPercentage.scale, - - iva_amount_value: source.ivaAmount.value, - iva_amount_scale: source.ivaAmount.scale, - - // REC - rec_code: maybeToNullableString(source.recCode), - - rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value), - rec_percentage_scale: - maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? - DiscountPercentage.DEFAULT_SCALE, - - rec_amount_value: source.recAmount.toPrimitive().value, - rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE, - - // RET - retention_code: maybeToNullableString(source.retentionCode), - - retention_percentage_value: maybeToNullable( - source.retentionPercentage, - (v) => v.toPrimitive().value - ), - retention_percentage_scale: - maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? - Percentage.DEFAULT_SCALE, - - retention_amount_value: source.retentionAmount.toPrimitive().value, - retention_amount_scale: - source.retentionAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE, - - // TOTAL - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, - }; - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts deleted file mode 100644 index 014a9304..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; -import { - City, - Country, - Name, - PostalCode, - Province, - Street, - TINNumber, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { IssuedInvoiceSummary } from "../../../../../../application"; -import { IssuedInvoiceRecipient } from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMapper< - CustomerInvoiceModel, - IssuedInvoiceRecipient -> { - public mapToReadModel( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - /** - * - Issued invoice => snapshot de los datos (campos customer_*) - */ - - const { errors, attributes } = params as { - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - const { isProforma } = attributes; - - if (isProforma && !raw.current_customer) { - errors.push({ - path: "current_customer", - message: "Current customer not included in query (InvoiceRecipientListMapper)", - }); - } - - const _name = raw.customer_name!; - const _tin = raw.customer_tin!; - const _street = raw.customer_street!; - const _street2 = raw.customer_street2!; - const _city = raw.customer_city!; - const _postal_code = raw.customer_postal_code!; - const _province = raw.customer_province!; - const _country = raw.customer_country!; - - // Customer (snapshot) - const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); - - const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); - - const customerStreet = extractOrPushError(Street.create(_street), "customer_street", errors); - - const customerStreet2 = extractOrPushError( - maybeFromNullableResult(_street2, (value) => Street.create(value)), - "customer_street2", - errors - ); - - const customerCity = extractOrPushError(City.create(_city), "customer_city", errors); - - const customerProvince = extractOrPushError( - maybeFromNullableResult(_province, (value) => Province.create(value)), - "customer_province", - errors - ); - - const customerPostalCode = extractOrPushError( - PostalCode.create(_postal_code), - "customer_postal_code", - errors - ); - - const customerCountry = extractOrPushError( - maybeFromNullableResult(_country, (value) => Country.create(value)), - "customer_country", - errors - ); - - const createResult = IssuedInvoiceRecipient.create({ - name: customerName!, - tin: customerTin!, - street: customerStreet!, - street2: customerStreet2!, - city: customerCity!, - postalCode: customerPostalCode!, - province: customerProvince!, - country: customerCountry!, - }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice recipient entity creation failed", [ - { path: "recipient", message: createResult.error.message }, - ]) - ); - } - - return Result.ok(createResult.data); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-summary.mapper.ts deleted file mode 100644 index c4fc13f5..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-summary.mapper.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; -import { - CurrencyCode, - LanguageCode, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import type { IssuedInvoiceSummary } from "../../../../../../application"; -import { - InvoiceAmount, - InvoiceNumber, - InvoiceSerie, - InvoiceStatus, - type VerifactuRecord, -} from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-invoice-recipient-summary.mapper"; -import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-summary.mapper"; - -export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper< - CustomerInvoiceModel, - IssuedInvoiceSummary -> { - private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper; - private _verifactuMapper: SequelizeVerifactuRecordSummaryMapper; - - constructor() { - super(); - this._recipientMapper = new SequelizeIssuedInvoiceRecipientListMapper(); - this._verifactuMapper = new SequelizeVerifactuRecordSummaryMapper(); - } - - public mapToReadModel( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - const errors: ValidationErrorDetail[] = []; - - // 1) Valores escalares (atributos generales) - const attributes = this._mapAttributesToReadModel(raw, { errors, ...params }); - - // 2) Recipient (snapshot en la factura o include) - const recipientResult = this._recipientMapper.mapToReadModel(raw, { - errors, - attributes, - ...params, - }); - - if (recipientResult.isFailure) { - errors.push({ - path: "recipient", - message: recipientResult.error.message, - }); - } - - // 4) Verifactu record - let verifactu: Maybe = Maybe.none(); - if (raw.verifactu) { - const verifactuResult = this._verifactuMapper.mapToReadModel(raw.verifactu, { - errors, - ...params, - }); - - if (verifactuResult.isFailure) { - errors.push({ - path: "verifactu", - message: verifactuResult.error.message, - }); - } else { - verifactu = Maybe.some(verifactuResult.data); - } - } - - // 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 [mapToDTO]", errors) - ); - } - - return Result.ok({ - id: attributes.invoiceId!, - companyId: attributes.companyId!, - isProforma: attributes.isProforma, - status: attributes.status!, - series: attributes.series!, - invoiceNumber: attributes.invoiceNumber!, - invoiceReference: attributes.invoiceReference!, - invoiceDate: attributes.invoiceDate!, - operationDate: attributes.operationDate!, - - description: attributes.description!, - reference: attributes.reference!, - - customerId: attributes.customerId!, - recipient: recipientResult.data, - - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - - subtotalAmount: attributes.subtotalAmount!, - totalDiscountAmount: attributes.totalDiscountAmount!, - taxableAmount: attributes.taxableAmount!, - taxesAmount: attributes.taxesAmount!, - totalAmount: attributes.totalAmount!, - - linkedProformaId: attributes.linkedProformaId!, - - verifactu, - }); - } - - private _mapAttributesToReadModel(raw: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); - - const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); - - const isProforma = Boolean(raw.is_proforma); - - const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); - - const series = extractOrPushError( - maybeFromNullableResult(raw.series, (value) => InvoiceSerie.create(value)), - "serie", - errors - ); - - const invoiceNumber = extractOrPushError( - InvoiceNumber.create(raw.invoice_number), - "invoice_number", - errors - ); - const invoiceReference = extractOrPushError( - Result.ok(`${String(raw.series ?? "")}${String(raw.invoice_number ?? "")}`), - "invoice_reference", - errors - ); - - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(raw.invoice_date), - "invoice_date", - errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - - const reference = extractOrPushError( - maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), - "description", - errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))), - "description", - errors - ); - - const languageCode = extractOrPushError( - LanguageCode.create(raw.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(raw.currency_code), - "currency_code", - errors - ); - - const subtotalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.subtotal_amount_value, - currency_code: currencyCode?.code, - }), - "subtotal_amount_value", - errors - ); - - const totalDiscountAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_discount_amount_value, - currency_code: currencyCode?.code, - }), - "total_discount_amount_value", - errors - ); - - const taxableAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxable_amount_value, - currency_code: currencyCode?.code, - }), - "taxable_amount_value", - errors - ); - - const taxesAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxes_amount_value, - currency_code: currencyCode?.code, - }), - "taxes_amount_value", - errors - ); - - const totalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_amount_value, - currency_code: currencyCode?.code, - }), - "total_amount_value", - errors - ); - - const linkedProformaId = extractOrPushError( - maybeFromNullableResult(raw.proforma_id, (value) => UniqueID.create(value)), - "linked_proforma_id", - errors - ); - - return { - invoiceId, - companyId, - customerId, - isProforma, - status, - series, - invoiceNumber, - invoiceReference, - invoiceDate, - operationDate, - reference, - description, - languageCode, - currencyCode, - - subtotalAmount, - totalDiscountAmount, - taxableAmount, - taxesAmount, - totalAmount, - - linkedProformaId, - }; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts index 8514958e..458ca377 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts @@ -1,2 +1 @@ -export * from "./issued-invoice.repository"; export * from "./issued-invoice-v2.repository"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts deleted file mode 100644 index b7f1ea4d..00000000 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; -import { - type Criteria, - CriteriaToSequelizeConverter, - getCountIncludes, - normalizeInclude, - normalizeOrder, -} from "@repo/rdx-criteria/server"; -import type { UniqueID } from "@repo/rdx-ddd"; -import { type Collection, Result } from "@repo/rdx-utils"; -import { - type CountOptions, - type FindOptions, - type Includeable, - type InferAttributes, - Op, - type OrderItem, - type Sequelize, - type Transaction, - UniqueConstraintError, - type WhereOptions, -} from "sequelize"; - -import { - IssuedInvoiceNumberAlreadyExistsError, - type IIssuedInvoiceRepository, - type IssuedInvoiceSummary, -} from "../../../../../application"; -import type { IssuedInvoice } from "../../../../../domain"; -import { - CustomerInvoiceItemModel, - CustomerInvoiceModel, - CustomerInvoiceTaxModel, - VerifactuRecordModel, -} from "../../../../common"; -import type { - SequelizeIssuedInvoiceDomainMapper, - SequelizeIssuedInvoiceSummaryMapper, -} from "../mappers"; - -export class IssuedInvoiceRepository - extends SequelizeRepository - implements IIssuedInvoiceRepository -{ - private static readonly INVOICE_NUMBER_UNIQUE_INDEX = "idx_invoice_company_series_number"; - - constructor( - private readonly domainMapper: SequelizeIssuedInvoiceDomainMapper, - private readonly summaryMapper: SequelizeIssuedInvoiceSummaryMapper, - database: Sequelize - ) { - super({ database }); - } - - /** - * - * Crea una nueva factura. - * - * @param invoice - El agregado a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - async create(invoice: IssuedInvoice, transaction?: Transaction): Promise> { - try { - const dtoResult = this.domainMapper.mapToPersistence(invoice); - - if (dtoResult.isFailure) { - return Result.fail(dtoResult.error); - } - - const dto = dtoResult.data; - const { id, items, taxes, verifactu, ...createPayload } = dto; - - // 1. Insertar cabecera - await CustomerInvoiceModel.create( - { - ...createPayload, - id, - }, - { transaction } - ); - - // 2. Inserta taxes de cabecera - if (Array.isArray(taxes) && taxes.length > 0) { - await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction }); - } - - // 3. Inserta items + sus taxes - if (Array.isArray(items) && items.length > 0) { - for (const item of items) { - await CustomerInvoiceItemModel.create(item, { transaction }); - } - } - - // 4. Inserta VerifactuRecord si existe - if (verifactu) { - await VerifactuRecordModel.create(verifactu, { transaction }); - } - - return Result.ok(); - } catch (err: unknown) { - if (this.isIssuedInvoiceNumberAlreadyExistsError(err)) { - return Result.fail(new IssuedInvoiceNumberAlreadyExistsError({ cause: err as Error })); - } - - return Result.fail(translateSequelizeError(err)); - } - } - - private isIssuedInvoiceNumberAlreadyExistsError(error: unknown): boolean { - if (!(error instanceof UniqueConstraintError)) { - return false; - } - - const fields = Object.keys(error.fields ?? {}); - const hasExpectedFields = - fields.includes("company_id") && - fields.includes("series") && - fields.includes("invoice_number") && - fields.includes("is_proforma"); - - const byIndexName = error.errors.some( - (detail) => detail.validatorKey === "not_unique" && detail.path === "idx_invoice_company_series_number" - ); - - return hasExpectedFields || byIndexName || error.message.includes(IssuedInvoiceRepository.INVOICE_NUMBER_UNIQUE_INDEX); - } - - /** - * Comprueba si existe una factura con un `id` dentro de una `company`. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - Identificador UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async existsByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - try { - const count = await CustomerInvoiceModel.count({ - ...options, - where: { - id: id.toString(), - company_id: companyId.toString(), - is_proforma: false, - ...(options.where ?? {}), - }, - transaction, - }); - return Result.ok(Boolean(count > 0)); - } catch (error: unknown) { - return Result.fail(translateSequelizeError(error)); - } - } - - /** - * - * Busca una factura por su identificador único. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async getByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - const { CustomerModel } = this.database.models; - - try { - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - const mergedOptions: FindOptions> = { - ...options, - where: { - ...(options.where ?? {}), - id: id.toString(), - is_proforma: false, - company_id: companyId.toString(), - }, - order: [ - ...normalizedOrder, - [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], - ], - include: [ - ...normalizedInclude, - { - model: VerifactuRecordModel, - as: "verifactu", - required: false, - attributes: ["id", "estado", "url", "uuid", "qr"], - }, - { - model: CustomerModel, - as: "current_customer", - required: false, - }, - { - model: CustomerInvoiceItemModel, - as: "items", - required: false, - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - }, - ], - transaction, - }; - - const row = await CustomerInvoiceModel.findOne(mergedOptions); - - if (!row) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); - } - - const invoice = this.domainMapper.mapToDomain(row); - return invoice; - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result - * - * @see Criteria - */ - public async findByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: Transaction, - options: FindOptions> = {} - ): Promise, Error>> { - const { CustomerModel } = this.database.models; - - try { - const criteriaConverter = new CriteriaToSequelizeConverter(); - - /** - * El converter solo traduce Criteria a piezas Sequelize genéricas: - * filtros, quick search, orden y paginación. - * - */ - const criteriaQuery = criteriaConverter.convert(criteria, { - searchableFields: ["invoice_number", "reference", "description"], - mappings: { - invoice_date: { - type: "root", - column: "invoice_date", - }, - invoice_number: { - type: "root", - column: "invoice_number", - }, - reference: { - type: "root", - column: "reference", - }, - description: { - type: "root", - column: "description", - }, - recipient_name: { - type: "root", - column: "recipient_name", - }, - verifactu_status: { - type: "association", - association: "verifactu", - column: "estado", - }, - }, - sortableFields: [ - "recipient_name", - "invoice_number", - "invoice_date", - "id", - "created_at", - "serie", - ], - fullTextTableAlias: "CustomerInvoiceModel", - enableFullText: true, - database: this.database, - strictMode: true, // fuerza error si ORDER BY no permitido - }); - - /** - * Restricciones propias del repositorio. - * - * El dominio `IssuedInvoice` no conoce `is_proforma`; este discriminador - * pertenece a la infraestructura porque actualmente se comparte tabla - * con proformas. - * - * No se añade `deleted_at: null` porque el modelo usa `paranoid: true`; - * Sequelize aplica automáticamente `deleted_at IS NULL` en las consultas. - */ - const baseWhere: WhereOptions> = { - is_proforma: false, - company_id: companyId.toString(), - }; - - /** - * Composición defensiva del WHERE. - * - * Se usa `Op.and` para no sobrescribir condiciones si `criteriaQuery.where` - * u `options.where` contienen operadores Sequelize. - */ - const where: WhereOptions> = { - [Op.and]: [ - baseWhere, - ...(options.where ? [options.where] : []), - ...(criteriaQuery.where ? [criteriaQuery.where] : []), - ], - }; - - /** - * Includes necesarios para materializar el resumen. - * - * `current_customer` también puede ser necesario para filtrar/ordenar por - * `recipient_name`, por eso se añade siempre. - */ - const include: Includeable[] = [ - ...normalizeInclude(options.include), - { - model: VerifactuRecordModel, - as: "verifactu", - required: false, - attributes: ["id", "estado", "url", "uuid", "qr"], - }, - { - model: CustomerModel, - as: "current_customer", - required: false, // false => LEFT JOIN - attributes: [ - "name", - "trade_name", - "tin", - "street", - "street2", - "city", - "postal_code", - "province", - "country", - ], - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - - /** - * Evita multiplicar filas de la query principal. - * - * Sequelize ejecuta una consulta separada para los impuestos y garantiza - * que `taxes` llegue como array. - */ - separate: true, - }, - { - model: CustomerInvoiceModel, - as: "proforma", - required: false, - attributes: ["id"], - }, - ]; - - /** - * Orden final. - * - * Primero va el orden derivado de Criteria: - * - score FULLTEXT si hay quick search; - * - orderBy/orderType del cliente si viene informado. - * - * Después se añade el orden técnico recibido por `options`. - */ - const order: OrderItem[] = [ - ...normalizeOrder(criteriaQuery.order), - ...normalizeOrder(options.order), - ]; - - /** - * Query principal. - * - * `options` se expande primero para permitir opciones técnicas externas, - * pero `criteriaQuery`, `where`, `include`, `order` y `transaction` - * prevalecen explícitamente. - */ - const findQuery: FindOptions> = { - ...options, - ...criteriaQuery, - where, - include, - order, - transaction, - }; - - /** - * Query de conteo. - * - * No se incluyen asociaciones `separate`, porque no participan en el - * filtrado de filas principales y añadirían coste sin aportar precisión. - * - * `count()` no usa `FindOptions`, sino `CountOptions`; por eso `distinct` - * y `col` deben tiparse con `CountOptions`. - */ - const countQuery: CountOptions> = { - where, - include: getCountIncludes(include), - distinct: true, - col: "id", - transaction, - }; - - const [rows, count] = await Promise.all([ - CustomerInvoiceModel.findAll(findQuery), - CustomerInvoiceModel.count(countQuery), - ]); - - return this.summaryMapper.mapToReadModelCollection(rows, count); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts index db6296f8..ddec7d63 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts @@ -1,9 +1,6 @@ export * from "./proforma-public-services"; export * from "./proforma-persistence.di"; export * from "./proforma-persistence-mappers-v2.di"; -export * from "./proforma-persistence-mappers.di"; export * from "./proforma-number-generator-v2.di"; -export * from "./proforma-number-generator.di"; export * from "./proforma-repositories-v2.di"; -export * from "./proforma-repositories.di"; export * from "./proformas.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts deleted file mode 100644 index 540c8112..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IProformaNumberGenerator } from "../../../application"; -import { SequelizeProformaNumberGenerator } from "../persistence"; - -export const buildLegacyProformaNumberGenerator = (): IProformaNumberGenerator => - new SequelizeProformaNumberGenerator(); - -export const buildProformaNumberGenerator = buildLegacyProformaNumberGenerator; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts deleted file mode 100644 index 92fa9416..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence-mappers.di.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../persistence"; - -export interface IProformaPersistenceMappers { - domainMapper: SequelizeProformaDomainMapper; - listMapper: SequelizeProformaSummaryMapper; -} - -export const buildLegacyProformaPersistenceMappers = (): IProformaPersistenceMappers => { - // Mappers para el repositorio - const domainMapper = new SequelizeProformaDomainMapper(); - const listMapper = new SequelizeProformaSummaryMapper(); - - return { - domainMapper, - listMapper, - }; -}; - -export const buildProformaPersistenceMappers = buildLegacyProformaPersistenceMappers; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence.di.ts index 8ac76206..6576f764 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-persistence.di.ts @@ -13,13 +13,6 @@ type BuildProformaPersistenceParams = { documentSeriesServices: DocumentSeriesPublicServicesType; }; -/*export type LegacyProformaPersistence = { - mode: "legacy"; - mappers: IProformaPersistenceMappers; - repository: ReturnType; - numberGenerator: ReturnType; -};*/ - export type V2ProformaPersistence = { mode: "v2"; mappers: IProformaPersistenceMappersV2; @@ -27,22 +20,6 @@ export type V2ProformaPersistence = { numberGenerator: ReturnType; }; -/*export const buildLegacyProformaPersistence = ( - params: BuildProformaPersistenceParams -): LegacyProformaPersistence => { - const mappers = buildLegacyProformaPersistenceMappers(); - - return { - mode: "legacy", - mappers, - repository: buildLegacyProformaRepository({ - database: params.database, - mappers, - }), - numberGenerator: buildLegacyProformaNumberGenerator(), - }; -};*/ - export const buildV2ProformaPersistence = ( params: BuildProformaPersistenceParams ): V2ProformaPersistence => { diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts deleted file mode 100644 index af399bef..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Sequelize } from "sequelize"; - -import { ProformaRepository } from "../persistence"; - -import type { IProformaPersistenceMappers } from "./proforma-persistence-mappers.di"; - -export const buildLegacyProformaRepository = (params: { - database: Sequelize; - mappers: IProformaPersistenceMappers; -}) => { - const { database, mappers } = params; - - return new ProformaRepository(mappers.domainMapper, mappers.listMapper, database); -}; - -export const buildProformaRepository = buildLegacyProformaRepository; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts index bc91616b..00642885 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts @@ -1,4 +1,3 @@ -export * from "./sequelize-proforma-domain.mapper"; export * from "./sequelize-proforma-v2-domain.mapper"; export * from "./sequelize-proforma-v2-item-domain.mapper"; export * from "./sequelize-proforma-v2-recipient-domain.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts deleted file mode 100644 index 2e2553cb..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; -import { - CurrencyCode, - LanguageCode, - TextValue, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, - maybeToNullable, -} from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import { - InvoiceNumber, - InvoiceSerie, - InvoiceStatus, - Proforma, - type ProformaInternalProps, - ProformaItems, - ProformaTaxConfig, -} from "../../../../../../domain"; -import type { - CustomerInvoiceCreationAttributes, - CustomerInvoiceModel, -} from "../../../../../common"; - -import { SequelizeProformaItemDomainMapper } from "./sequelize-proforma-item-domain.mapper"; -import { SequelizeProformaRecipientDomainMapper } from "./sequelize-proforma-recipient-domain.mapper"; -import { SequelizeProformaTaxesDomainMapper } from "./sequelize-proforma-taxes-domain.mapper"; - -export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceModel, - CustomerInvoiceCreationAttributes, - Proforma -> { - private _itemsMapper: SequelizeProformaItemDomainMapper; - private _recipientMapper: SequelizeProformaRecipientDomainMapper; - private _taxesMapper: SequelizeProformaTaxesDomainMapper; - - constructor(params?: MapperParamsType) { - super(); - - this._itemsMapper = new SequelizeProformaItemDomainMapper(); - this._recipientMapper = new SequelizeProformaRecipientDomainMapper(); - this._taxesMapper = new SequelizeProformaTaxesDomainMapper(); - } - - private _mapAttributesToDomain(raw: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); - - const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); - - const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); - - const series = extractOrPushError( - maybeFromNullableResult(raw.series, (value) => InvoiceSerie.create(value)), - "series", - errors - ); - - const invoiceNumber = extractOrPushError( - InvoiceNumber.create(raw.invoice_number), - "invoice_number", - errors - ); - - // Fechas - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(raw.invoice_date), - "invoice_date", - errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - - // Idioma / divisa - const languageCode = extractOrPushError( - LanguageCode.create(raw.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(raw.currency_code), - "currency_code", - errors - ); - - // Textos opcionales - const reference = extractOrPushError( - maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), - "reference", - errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))), - "description", - errors - ); - - const notes = extractOrPushError( - maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)), - "notes", - errors - ); - - // Método de pago (VO opcional con id + descripción) - /*let paymentMethod = Maybe.none(); - - if (!isNullishOrEmpty(raw.payment_method_id)) { - const paymentId = extractOrPushError( - UniqueID.create(String(raw.payment_method_id)), - "paymentMethod.id", - errors - ); - - if (paymentId) { - const paymentOrNot = this._paymentCatalog.findById(paymentId.toString()); - - if (paymentOrNot.isSome()) { - const paymentCatalogItem = paymentOrNot.unwrap(); - - const paymentVO = extractOrPushError( - InvoicePaymentMethod.create( - { paymentDescription: paymentCatalogItem.description ?? "" }, - paymentId - ), - "paymentMethod", - errors - ); - - if (paymentVO) { - paymentMethod = Maybe.some(paymentVO); - } - } - } - }*/ - - // Método de pago (ID) - const paymentMethodId = extractOrPushError( - maybeFromNullableResult(raw.payment_method_id, (value) => UniqueID.create(String(value))), - "payment_method_id", - errors - ); - - // Tax regime code - const taxRegimeCode = extractOrPushError( - maybeFromNullableResult(raw.tax_regime_code, (value) => Result.ok(String(value))), - "tax_regime_code", - errors - ); - - // % descuento global (VO) - const globalDiscountPercentage = extractOrPushError( - DiscountPercentage.create({ - value: Number(raw.global_discount_percentage_value ?? 0), - }), - "global_discount_percentage_value", - errors - ); - - const linkedInvoiceId = extractOrPushError( - maybeFromNullableResult(raw.linked_invoice?.id, (value) => UniqueID.create(value)), - "linked_invoice_id", - errors - ); - - const taxConfig = extractOrPushError( - ProformaTaxConfig.create({ - taxMode: raw.tax_mode === "per_line" ? "perLine" : "single", - defaultIvaCode: maybeFromNullableResult(raw.default_iva_code, (value) => - Result.ok(String(value)) - ), - usesEquivalenceSurcharge: Boolean(raw.uses_equivalence_surcharge), - defaultRecCode: maybeFromNullableResult(raw.default_rec_code, (value) => - Result.ok(String(value)) - ), - usesRetention: Boolean(raw.uses_retention), - defaultRetentionCode: maybeFromNullableResult(raw.default_retention_code, (value) => - Result.ok(String(value)) - ), - }), - "tax_config", - errors - ); - - return { - invoiceId, - companyId, - customerId, - status, - series, - invoiceNumber, - invoiceDate, - operationDate, - reference, - description, - notes, - languageCode, - currencyCode, - paymentMethodId, - taxRegimeCode, - taxConfig, - - globalDiscountPercentage, - linkedInvoiceId, - }; - } - - public mapToDomain( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - try { - const errors: ValidationErrorDetail[] = []; - - // 1) Valores escalares (atributos generales) - const attributes = this._mapAttributesToDomain(raw, { errors, ...params }); - - // 2) Recipient (snapshot en la factura o include) - const recipientResult = this._recipientMapper.mapToDomain(raw, { - errors, - parent: attributes, - ...params, - }); - - // 3) Items (colección) - const itemCollectionResults = this._itemsMapper.mapToDomainCollection( - raw.items, - raw.items.length, - { - errors, - parent: attributes, - ...params, - } - ); - - // 4) Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - console.error(errors); - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) - ); - } - - // 6) Construcción del agregado (Dominio) - const items = ProformaItems.create({ - items: itemCollectionResults.data.getAll(), - }); - - const invoiceProps: ProformaInternalProps = { - companyId: attributes.companyId!, - - status: attributes.status!, - documentSeriesId: Maybe.none(), - proformaNumber: Maybe.some(attributes.invoiceNumber!), - targetInvoiceSeriesCode: attributes.series!, - proformaReference: attributes.invoiceNumber!, - proformaDate: attributes.invoiceDate!, - operationDate: attributes.operationDate!, - - customerId: attributes.customerId!, - recipient: recipientResult.data, - - reference: attributes.reference!, - description: attributes.description!, - notes: attributes.notes!, - - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - - globalDiscountPercentage: attributes.globalDiscountPercentage!, - - paymentMethodId: attributes.paymentMethodId!, - taxRegimeCode: attributes.taxRegimeCode!, - taxConfig: attributes.taxConfig!, - - linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia. - }; - - const proformaId = attributes.invoiceId!; - const proforma = Proforma.rehydrate(invoiceProps, items, proformaId); - - return Result.ok(proforma); - } catch (err: unknown) { - return Result.fail(err as Error); - } - } - - public mapToPersistence( - source: Proforma, - params?: MapperParamsType - ): Result { - const errors: ValidationErrorDetail[] = []; - - // 1) Items - const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { - errors, - parent: source, - ...params, - }); - - if (itemsResult.isFailure) { - errors.push({ - path: "items", - message: itemsResult.error.message, - }); - } - - // 2) Taxes - const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes(), { - errors, - parent: source, - ...params, - }); - - if (taxesResult.isFailure) { - errors.push({ - path: "taxes", - message: taxesResult.error.message, - }); - } - - // 3) Cliente - const recipient = this._recipientMapper.mapToPersistence(source.recipient, { - errors, - parent: source, - ...params, - }); - - // 4) 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 items = itemsResult.data; - const taxes = taxesResult.data; - - const allAmounts = source.totals(); // Da los totales ya calculados - - const proformaValues: CustomerInvoiceCreationAttributes = { - // Identificación - id: source.id.toPrimitive(), - company_id: source.companyId.toPrimitive(), - - // Flags / estado / serie / número - is_proforma: true, - status: source.status.toPrimitive(), - proforma_id: null, - - series: maybeToNullable(source.targetInvoiceSeriesCode, (v) => v.toPrimitive()), - invoice_number: source.proformaReference.toPrimitive(), - invoice_date: source.proformaDate.toPrimitive(), - operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()), - language_code: source.languageCode.toPrimitive(), - currency_code: source.currencyCode.toPrimitive(), - - reference: maybeToNullable(source.reference, (reference) => reference), - description: maybeToNullable(source.description, (description) => description), - notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), - - payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()), - payment_method_description: null, - - tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value), - tax_regime_description: null, - tax_mode: source.taxConfig.taxMode === "perLine" ? "per_line" : "single", - default_iva_code: maybeToNullable(source.taxConfig.defaultIvaCode, (value) => value), - uses_equivalence_surcharge: source.taxConfig.usesEquivalenceSurcharge, - default_rec_code: maybeToNullable(source.taxConfig.defaultRecCode, (value) => value), - uses_retention: source.taxConfig.usesRetention, - default_retention_code: maybeToNullable( - source.taxConfig.defaultRetentionCode, - (value) => value - ), - - subtotal_amount_value: allAmounts.subtotalAmount.value, - subtotal_amount_scale: allAmounts.subtotalAmount.scale, - - items_discount_amount_value: allAmounts.itemsDiscountAmount.value, - items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale, - - global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, - global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, - - global_discount_amount_value: allAmounts.globalDiscountAmount.value, - global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, - - total_discount_amount_value: allAmounts.totalDiscountAmount.value, - total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, - - taxable_amount_value: allAmounts.taxableAmount.value, - taxable_amount_scale: allAmounts.taxableAmount.scale, - - iva_amount_value: allAmounts.ivaAmount.value, - iva_amount_scale: allAmounts.ivaAmount.scale, - - rec_amount_value: allAmounts.recAmount.value, - rec_amount_scale: allAmounts.recAmount.scale, - - retention_amount_value: allAmounts.retentionAmount.value, - retention_amount_scale: allAmounts.retentionAmount.scale, - - taxes_amount_value: allAmounts.taxesAmount.value, - taxes_amount_scale: allAmounts.taxesAmount.scale, - - total_amount_value: allAmounts.totalAmount.value, - total_amount_scale: allAmounts.totalAmount.scale, - - customer_id: source.customerId.toPrimitive(), - ...recipient, - - taxes, - items, - }; - - return Result.ok( - proformaValues satisfies CustomerInvoiceCreationAttributes - ); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts deleted file mode 100644 index fe693001..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { - DiscountPercentage, - type MapperParamsType, - SequelizeDomainMapper, - Tax, - type TaxCalculationBehavior, - type TaxGroup, - TaxPercentage, -} from "@erp/core/api"; -import { - UniqueID, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, - maybeToNullable, -} from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import { - type IProformaCreateProps, - type IProformaItemCreateProps, - ItemAmount, - ItemDescription, - ItemQuantity, - type Proforma, - ProformaItem, - ProformaItemTaxes, - type ProformaItemTaxesProps, -} from "../../../../../../domain"; -import type { - CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItemModel, -} from "../../../../../common"; - -export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceItemModel, - CustomerInvoiceItemCreationAttributes, - ProformaItem -> { - private mapAttributesToDomain( - raw: CustomerInvoiceItemModel, - params?: MapperParamsType - ): Partial & { itemId?: UniqueID } { - const { errors, index, parent } = params as { - index: number; - errors: ValidationErrorDetail[]; - parent: Partial; - }; - - const { currencyCode } = parent; - - const itemId = extractOrPushError( - UniqueID.create(raw.item_id), - `items[${index}].item_id`, - errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(raw.description, (value) => ItemDescription.create(value)), - `items[${index}].description`, - errors - ); - - const quantity = extractOrPushError( - maybeFromNullableResult(raw.quantity_value, (value) => - ItemQuantity.create({ value, scale: raw.quantity_scale }) - ), - `items[${index}].quantity_value`, - errors - ); - - const unitAmount = extractOrPushError( - maybeFromNullableResult(raw.unit_amount_value, (value) => - ItemAmount.create({ - value, - currency_code: currencyCode?.code, - scale: raw.unit_amount_scale, - }) - ), - `items[${index}].unit_amount_value`, - errors - ); - - const itemDiscountPercentage = extractOrPushError( - maybeFromNullableResult(raw.item_discount_percentage_value, (value) => - DiscountPercentage.create({ - value, - scale: raw.item_discount_percentage_scale, - }) - ), - `items[${index}].item_discount_percentage`, - errors - ); - - const iva = extractOrPushError( - this.mapTaxToDomain({ - code: raw.iva_code, - percentageValue: raw.iva_percentage_value, - percentageScale: raw.iva_percentage_scale, - group: "iva", - calculationBehavior: "additive", - fieldPath: `items[${index}].iva`, - }), - `items[${index}].iva`, - errors - ); - - const rec = extractOrPushError( - this.mapTaxToDomain({ - code: raw.rec_code, - percentageValue: raw.rec_percentage_value, - percentageScale: raw.rec_percentage_scale, - group: "surcharge", - calculationBehavior: "additive", - fieldPath: `items[${index}].rec`, - }), - `items[${index}].rec`, - errors - ); - - const retention = extractOrPushError( - this.mapTaxToDomain({ - code: raw.retention_code, - percentageValue: raw.retention_percentage_value, - percentageScale: raw.retention_percentage_scale, - group: "retention", - calculationBehavior: "subtractive", - fieldPath: `items[${index}].retention`, - }), - `items[${index}].retention`, - errors - ); - - return { - itemId, - description, - quantity, - unitAmount, - itemDiscountPercentage, - iva, - rec, - retention, - }; - } - - private mapTaxToDomain(params: { - code: string | null; - percentageValue: number | null; - percentageScale?: number; - group: TaxGroup; - calculationBehavior: TaxCalculationBehavior; - fieldPath: string; - }): Result, Error> { - if (params.code === null || params.code.trim() === "") { - return Result.ok(Maybe.none()); - } - - if (params.percentageValue === null) { - return Result.fail( - new Error(`${params.fieldPath}.percentage_value is required when tax code is present`) - ); - } - - const percentageResult = TaxPercentage.create({ - value: params.percentageValue, - }); - - if (percentageResult.isFailure) { - return Result.fail(percentageResult.error); - } - - const taxResult = Tax.create({ - code: params.code, - name: params.code, - rate: percentageResult.data, - group: params.group, - calculationBehavior: params.calculationBehavior, - }); - - if (taxResult.isFailure) { - return Result.fail(taxResult.error); - } - - return Result.ok(Maybe.some(taxResult.data)); - } - - public mapToDomain( - raw: CustomerInvoiceItemModel, - params?: MapperParamsType - ): Result { - const { errors } = params as { - index: number; - errors: ValidationErrorDetail[]; - parent: Partial; - }; - - const attributes = this.mapAttributesToDomain(raw, params); - - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) - ); - } - - const taxesResult = ProformaItemTaxes.create({ - iva: attributes.iva!, - rec: attributes.rec!, - retention: attributes.retention!, - }); - - if (taxesResult.isFailure) { - return Result.fail(taxesResult.error); - } - - const item = ProformaItem.rehydrate( - { - description: attributes.description!, - quantity: attributes.quantity!, - unitAmount: attributes.unitAmount!, - itemDiscountPercentage: attributes.itemDiscountPercentage!, - taxes: taxesResult.data, - }, - attributes.itemId! - ); - - return Result.ok(item); - } - - public mapToPersistence( - source: ProformaItem, - params?: MapperParamsType - ): Result { - const { index, parent } = params as { - index: number; - parent: Proforma; - errors: ValidationErrorDetail[]; - }; - - const allAmounts = source.totals({ - currencyCode: parent.currencyCode, - globalDiscountPercentage: parent.globalDiscountPercentage, - languageCode: parent.languageCode, - }); - - return Result.ok({ - item_id: source.id.toPrimitive(), - invoice_id: parent.id.toPrimitive(), - position: index, - - description: maybeToNullable(source.description, (value) => value.toPrimitive()), - - quantity_value: maybeToNullable(source.quantity, (value) => value.toPrimitive().value), - quantity_scale: - maybeToNullable(source.quantity, (value) => value.toPrimitive().scale) ?? - ItemQuantity.DEFAULT_SCALE, - - unit_amount_value: maybeToNullable(source.unitAmount, (value) => value.toPrimitive().value), - unit_amount_scale: - maybeToNullable(source.unitAmount, (value) => value.toPrimitive().scale) ?? - ItemAmount.DEFAULT_SCALE, - - subtotal_amount_value: allAmounts.subtotalAmount.value, - subtotal_amount_scale: allAmounts.subtotalAmount.scale, - - item_discount_percentage_value: maybeToNullable( - source.itemDiscountPercentage, - (value) => value.toPrimitive().value - ), - item_discount_percentage_scale: - maybeToNullable(source.itemDiscountPercentage, (value) => value.toPrimitive().scale) ?? - DiscountPercentage.DEFAULT_SCALE, - - item_discount_amount_value: allAmounts.itemDiscountAmount.value, - item_discount_amount_scale: allAmounts.itemDiscountAmount.scale, - - global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value, - global_discount_percentage_scale: - parent.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE, - - global_discount_amount_value: allAmounts.globalDiscountAmount.value, - global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, - - total_discount_amount_value: allAmounts.totalDiscountAmount.value, - total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, - - taxable_amount_value: allAmounts.taxableAmount.value, - taxable_amount_scale: allAmounts.taxableAmount.scale, - - iva_code: maybeToNullable(source.taxes.iva, (value) => value.code), - iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value), - iva_percentage_scale: - maybeToNullable(source.taxes.iva, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, - iva_amount_value: allAmounts.ivaAmount.value, - iva_amount_scale: allAmounts.ivaAmount.scale, - - rec_code: maybeToNullable(source.taxes.rec, (value) => value.code), - rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value), - rec_percentage_scale: - maybeToNullable(source.taxes.rec, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, - rec_amount_value: allAmounts.recAmount.value, - rec_amount_scale: allAmounts.recAmount.scale, - - retention_code: maybeToNullable(source.taxes.retention, (value) => value.code), - retention_percentage_value: maybeToNullable( - source.taxes.retention, - (value) => value.percentage.value - ), - retention_percentage_scale: - maybeToNullable(source.taxes.retention, (value) => value.percentage.scale) ?? - Tax.DEFAULT_SCALE, - retention_amount_value: allAmounts.retentionAmount.value, - retention_amount_scale: allAmounts.retentionAmount.scale, - - taxes_amount_value: allAmounts.taxesAmount.value, - taxes_amount_scale: allAmounts.taxesAmount.scale, - - total_amount_value: allAmounts.totalAmount.value, - total_amount_scale: allAmounts.totalAmount.scale, - }); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts deleted file mode 100644 index 53dc08e7..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { MapperParamsType } from "@erp/core/api"; -import { - City, - Country, - Name, - PostalCode, - Province, - Street, - TINNumber, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import { type IProformaCreateProps, ProformaRecipient } from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -export class SequelizeProformaRecipientDomainMapper { - public mapToDomain( - source: CustomerInvoiceModel, - params?: MapperParamsType - ): Result, Error> { - /** - * En proforma -> datos de "current_customer" - */ - - const { errors, parent } = params as { - errors: ValidationErrorDetail[]; - parent: Partial; - }; - - const _name = source.current_customer?.name; - const _tin = source.current_customer?.tin; - const _street = source.current_customer?.street; - const _street2 = source.current_customer?.street2; - const _city = source.current_customer?.city; - const _postal_code = source.current_customer?.postal_code; - const _province = source.current_customer?.province; - const _country = source.current_customer?.country; - - // Customer (snapshot) - const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); - - const customerTin = extractOrPushError( - maybeFromNullableResult(_tin, (value) => TINNumber.create(value)), - "customer_tin", - errors - ); - - const customerStreet = extractOrPushError( - maybeFromNullableResult(_street, (value) => Street.create(value)), - "customer_street", - errors - ); - - const customerStreet2 = extractOrPushError( - maybeFromNullableResult(_street2, (value) => Street.create(value)), - "customer_street2", - errors - ); - - const customerCity = extractOrPushError( - maybeFromNullableResult(_city, (value) => City.create(value)), - "customer_city", - errors - ); - - const customerProvince = extractOrPushError( - maybeFromNullableResult(_province, (value) => Province.create(value)), - "customer_province", - errors - ); - - const customerPostalCode = extractOrPushError( - maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)), - "customer_postal_code", - errors - ); - - const customerCountry = extractOrPushError( - maybeFromNullableResult(_country, (value) => Country.create(value)), - "customer_country", - errors - ); - - const createResult = ProformaRecipient.create({ - name: customerName!, - tin: customerTin!, - street: customerStreet!, - street2: customerStreet2!, - city: customerCity!, - postalCode: customerPostalCode!, - province: customerProvince!, - country: customerCountry!, - }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice recipient entity creation failed", [ - { path: "recipient", message: createResult.error.message }, - ]) - ); - } - - return Result.ok(Maybe.some(createResult.data)); - } - - /** - * Para una proforma, el recipient no se persiste (todos los campos son null). - */ - mapToPersistence(source: Maybe, params?: MapperParamsType) { - return { - 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, - }; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts deleted file mode 100644 index 5980e6a4..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api"; -import { UniqueID, type ValidationErrorDetail, maybeToNullable } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { IProformaTaxTotals, Proforma } from "../../../../../../domain"; -import type { - CustomerInvoiceTaxCreationAttributes, - CustomerInvoiceTaxModel, -} from "../../../../../common"; - -/** - * Mapper para customer_invoice_taxes - * - * Domina estructuras: - * { - * tax: Tax - * taxableAmount: ItemAmount - * taxesAmount: ItemAmount - * } - * - * Cada fila = un impuesto agregado en toda la factura. - */ -export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceTaxModel, - CustomerInvoiceTaxCreationAttributes, - IProformaTaxTotals -> { - public mapToDomain( - source: CustomerInvoiceTaxModel, - params?: MapperParamsType - ): Result { - throw new Error("Se calcula a partir de las líneas de detalle"); - } - - public mapToPersistence( - source: IProformaTaxTotals, - params?: MapperParamsType - ): Result { - const { errors, parent } = params as { - parent: Proforma; - errors: ValidationErrorDetail[]; - }; - - try { - const dto: CustomerInvoiceTaxCreationAttributes = { - tax_id: UniqueID.generateNewID().toPrimitive(), - invoice_id: parent.id.toPrimitive(), - - // TAXABLE AMOUNT - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - // IVA - iva_code: maybeToNullable(source.ivaCode, (v) => v), - - iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.value), - iva_percentage_scale: - maybeToNullable(source.ivaPercentage, (v) => v.scale) ?? TaxPercentage.DEFAULT_SCALE, - - iva_amount_value: source.ivaAmount.value, - iva_amount_scale: source.ivaAmount.scale, - - // REC - rec_code: maybeToNullable(source.recCode, (v) => v), - - rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.value), - rec_percentage_scale: - maybeToNullable(source.recPercentage, (v) => v.scale) ?? TaxPercentage.DEFAULT_SCALE, - - rec_amount_value: source.recAmount.value, - rec_amount_scale: source.recAmount.scale, - - // RET - retention_code: maybeToNullable(source.retentionCode, (v) => v), - - retention_percentage_value: maybeToNullable(source.retentionPercentage, (v) => v.value), - retention_percentage_scale: - maybeToNullable(source.retentionPercentage, (v) => v.scale) ?? - TaxPercentage.DEFAULT_SCALE, - - retention_amount_value: source.retentionAmount.value, - retention_amount_scale: source.retentionAmount.scale, - - // TOTAL - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, - }; - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/index.ts index 07765287..967aab81 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/index.ts @@ -1,3 +1,2 @@ -export * from "./sequelize-proforma-summary.mapper"; export * from "./sequelize-proforma-v2-summary.mapper"; export * from "./sequelize-proforma-v2-recipient-summary.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-recipient-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-recipient-summary.mapper.ts deleted file mode 100644 index 96c49792..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-recipient-summary.mapper.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; -import { - City, - Country, - Name, - PostalCode, - Province, - Street, - TINNumber, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { ProformaSummary } from "../../../../../../application"; -import { ProformaRecipient } from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -export class SequelizeInvoiceRecipientSummaryMapper extends SequelizeQueryMapper< - CustomerInvoiceModel, - ProformaRecipient -> { - public mapToReadModel( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - /** - * En proforma -> datos de "current_customer" - */ - - const { errors, attributes } = params as { - errors: ValidationErrorDetail[]; - attributes: Partial; - }; - - const { isProforma } = attributes; - - if (isProforma && !raw.current_customer) { - errors.push({ - path: "current_customer", - message: "Current customer not included in query (SequelizeInvoiceRecipientListMapper)", - }); - } - - const _name = raw.current_customer!.name; - const _tin = raw.current_customer!.tin; - const _street = raw.current_customer!.street; - const _street2 = raw.current_customer!.street2; - const _city = raw.current_customer!.city; - const _postal_code = raw.current_customer!.postal_code; - const _province = raw.current_customer!.province; - const _country = raw.current_customer!.country; - - // Customer (snapshot) - const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); - - //const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); - const customerTin = extractOrPushError( - maybeFromNullableResult(_tin, (value) => TINNumber.create(value)), - "customer_tin", - errors - ); - - const customerStreet = extractOrPushError( - maybeFromNullableResult(_street, (value) => Street.create(value)), - "customer_street", - errors - ); - - const customerStreet2 = extractOrPushError( - maybeFromNullableResult(_street2, (value) => Street.create(value)), - "customer_street2", - errors - ); - - const customerCity = extractOrPushError( - maybeFromNullableResult(_city, (value) => City.create(value)), - "customer_city", - errors - ); - - const customerProvince = extractOrPushError( - maybeFromNullableResult(_province, (value) => Province.create(value)), - "customer_province", - errors - ); - - const customerPostalCode = extractOrPushError( - maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)), - "customer_postal_code", - errors - ); - - const customerCountry = extractOrPushError( - maybeFromNullableResult(_country, (value) => Country.create(value)), - "customer_country", - errors - ); - - const createResult = ProformaRecipient.create({ - name: customerName!, - tin: customerTin!, - street: customerStreet!, - street2: customerStreet2!, - city: customerCity!, - postalCode: customerPostalCode!, - province: customerProvince!, - country: customerCountry!, - }); - - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice recipient entity creation failed", [ - { path: "recipient", message: createResult.error.message }, - ]) - ); - } - - return Result.ok(createResult.data); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts deleted file mode 100644 index 8d968129..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/summary/sequelize-proforma-summary.mapper.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; -import { - CurrencyCode, - LanguageCode, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { ProformaSummary } from "../../../../../../application"; -import { - InvoiceAmount, - InvoiceNumber, - InvoiceSerie, - InvoiceStatus, -} from "../../../../../../domain"; -import type { CustomerInvoiceModel } from "../../../../../common"; - -import { SequelizeInvoiceRecipientSummaryMapper } from "./sequelize-proforma-recipient-summary.mapper"; - -export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper< - CustomerInvoiceModel, - ProformaSummary -> { - private _recipientMapper: SequelizeInvoiceRecipientSummaryMapper; - - constructor() { - super(); - this._recipientMapper = new SequelizeInvoiceRecipientSummaryMapper(); - } - - public mapToReadModel( - raw: CustomerInvoiceModel, - params?: MapperParamsType - ): Result { - const errors: ValidationErrorDetail[] = []; - - // 1) Valores escalares (atributos generales) - const attributes = this.mapAttributesToDTO(raw, { errors, ...params }); - - // 2) Recipient (snapshot en la factura o include) - const recipientResult = this._recipientMapper.mapToReadModel(raw, { - errors, - attributes, - ...params, - }); - - if (recipientResult.isFailure) { - errors.push({ - path: "recipient", - message: recipientResult.error.message, - }); - } - - // 4) Si hubo errores de mapeo, devolvemos colección de validación - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping failed [mapToDTO]", errors) - ); - } - - return Result.ok({ - id: attributes.invoiceId!, - companyId: attributes.companyId!, - isProforma: attributes.isProforma, - status: attributes.status!, - series: attributes.series!, - proformaReference: attributes.proformaReference!, - proformaDate: attributes.proformaDate!, - operationDate: attributes.operationDate!, - - description: attributes.description!, - reference: attributes.description!, - - customerId: attributes.customerId!, - recipient: recipientResult.data, - - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, - - subtotalAmount: attributes.subtotalAmount!, - totalDiscountAmount: attributes.totalDiscountAmount!, - taxableAmount: attributes.taxableAmount!, - taxesAmount: attributes.taxesAmount!, - totalAmount: attributes.totalAmount!, - - linkedInvoiceId: attributes.linkedInvoiceId!, - }); - } - - private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); - - const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); - - const isProforma = Boolean(raw.is_proforma); - - const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); - - const series = extractOrPushError( - maybeFromNullableResult(raw.series, (value) => InvoiceSerie.create(value)), - "serie", - errors - ); - - const proformaReference = extractOrPushError( - InvoiceNumber.create(raw.invoice_number), - "proforma_reference", - errors - ); - - const proformaDate = extractOrPushError( - UtcDate.createFromISO(raw.invoice_date), - "proforma_date", - errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - - const reference = extractOrPushError( - maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))), - "description", - errors - ); - - const description = extractOrPushError( - maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))), - "description", - errors - ); - - const languageCode = extractOrPushError( - LanguageCode.create(raw.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(raw.currency_code), - "currency_code", - errors - ); - - const subtotalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.subtotal_amount_value, - currency_code: currencyCode?.code, - }), - "subtotal_amount_value", - errors - ); - - const totalDiscountAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_discount_amount_value, - currency_code: currencyCode?.code, - }), - "total_discount_amount_value", - errors - ); - - const taxableAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxable_amount_value, - currency_code: currencyCode?.code, - }), - "taxable_amount_value", - errors - ); - - const taxesAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.taxes_amount_value, - currency_code: currencyCode?.code, - }), - "taxes_amount_value", - errors - ); - - const totalAmount = extractOrPushError( - InvoiceAmount.create({ - value: raw.total_amount_value, - currency_code: currencyCode?.code, - }), - "total_amount_value", - errors - ); - - const linkedInvoiceId = extractOrPushError( - maybeFromNullableResult(raw.linked_invoice?.id, (value) => UniqueID.create(value)), - "linked_invoice_id", - errors - ); - - return { - invoiceId, - companyId, - customerId, - isProforma, - status, - series, - proformaReference, - proformaDate, - operationDate, - reference, - description, - languageCode, - currencyCode, - - subtotalAmount, - totalDiscountAmount, - taxableAmount, - taxesAmount, - totalAmount, - - linkedInvoiceId, - }; - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts index b3fbc422..0ada9793 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts @@ -1,2 +1 @@ -export * from "./proforma.repository"; export * from "./proforma-v2.repository"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts deleted file mode 100644 index ce981b2a..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ /dev/null @@ -1,762 +0,0 @@ -import { - EntityNotFoundError, - InfrastructureRepositoryError, - SequelizeRepository, - translateSequelizeError, -} from "@erp/core/api"; -import { - type Criteria, - CriteriaToSequelizeConverter, - getCountIncludes, - normalizeInclude, - normalizeOrder, -} from "@repo/rdx-criteria/server"; -import type { UniqueID } from "@repo/rdx-ddd"; -import { type Collection, Result } from "@repo/rdx-utils"; -import { - type CountOptions, - type FindOptions, - type Includeable, - type InferAttributes, - col, - fn, - Op, - type OrderItem, - type Sequelize, - type Transaction, - type WhereOptions, -} from "sequelize"; - -import type { - IProformaRepository, - ProformaStatusCounts, - ProformaSummary, -} from "../../../../../application"; -import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain"; -import { - CustomerInvoiceItemModel, - CustomerInvoiceModel, - CustomerInvoiceTaxModel, - IssuedInvoiceModel, -} from "../../../../common"; -import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers"; -import type { ProformaListScope } from "../../../../../application"; - -export class ProformaRepository - extends SequelizeRepository - implements IProformaRepository -{ - constructor( - private readonly domainMapper: SequelizeProformaDomainMapper, - private readonly summaryMapper: SequelizeProformaSummaryMapper, - database: Sequelize - ) { - super({ database }); - } - - /** - * - * Crea una nueva factura. - * - * @param proforma - El agregado a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - async create(proforma: Proforma, transaction?: Transaction): Promise> { - try { - const dtoResult = this.domainMapper.mapToPersistence(proforma); - - if (dtoResult.isFailure) { - return Result.fail(dtoResult.error); - } - - const dto = dtoResult.data; - const { id, items, taxes, ...createPayload } = dto; - - // 1. Insertar cabecera - await CustomerInvoiceModel.create( - { - ...createPayload, - id, - }, - { transaction } - ); - - // 2. Inserta taxes de cabecera - if (Array.isArray(taxes) && taxes.length > 0) { - await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction }); - } - - // 3. Inserta items + sus taxes - if (Array.isArray(items) && items.length > 0) { - for (const item of items) { - await CustomerInvoiceItemModel.create(item, { transaction }); - } - } - - return Result.ok(); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * Actualiza una factura existente. - * - * @param invoice - El agregado a actualizar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - async update(proforma: Proforma, transaction: Transaction): Promise> { - try { - const dtoResult = this.domainMapper.mapToPersistence(proforma); - - if (dtoResult.isFailure) return Result.fail(dtoResult.error); - - const dto = dtoResult.data; - const { id, items, taxes, ...updatePayload } = dto; - - // 1. Actualizar cabecera - const [affectedCount] = await CustomerInvoiceModel.update(updatePayload, { - where: { id /*, version */ }, - transaction, - }); - - if (affectedCount === 0) { - return Result.fail( - new InfrastructureRepositoryError(`Invoice ${id} not found or concurrency issue`) - ); - } - - // 2. Borra items y taxes previos (simplifica sincronización) - await CustomerInvoiceItemModel.destroy({ - where: { invoice_id: id }, - transaction, - }); - await CustomerInvoiceTaxModel.destroy({ - where: { invoice_id: id }, - transaction, - }); - - // 3. Inserta taxes de cabecera - if (Array.isArray(taxes) && taxes.length > 0) { - await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction }); - } - - // 4. Inserta items + sus taxes - if (Array.isArray(items) && items.length > 0) { - for (const item of items) { - await CustomerInvoiceItemModel.create(item, { transaction }); - } - } - - return Result.ok(); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * Comprueba si existe una factura con un `id` dentro de una `company`. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - Identificador UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async existsByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - try { - const count = await CustomerInvoiceModel.count({ - ...options, - where: { - id: id.toString(), - company_id: companyId.toString(), - is_proforma: true, - ...(options.where ?? {}), - }, - transaction, - }); - return Result.ok(Boolean(count > 0)); - } catch (error: unknown) { - return Result.fail(translateSequelizeError(error)); - } - } - - /** - * - * Elimina o marca como eliminada una proforma dentro de una empresa. - * - * @param companyId - ID de la empresa. - * @param id - UUID de la proforma a eliminar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - async deleteByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction - ): Promise> { - try { - const deleted = await CustomerInvoiceModel.destroy({ - where: { - id: id.toString(), - company_id: companyId.toString(), - is_proforma: true, - }, - transaction, - }); - - if (deleted === 0) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); - } - - return Result.ok(true); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - async existsIssuedInvoiceBySourceProformaIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction - ): Promise> { - try { - const count = await IssuedInvoiceModel.count({ - where: { - company_id: companyId.toString(), - source_proforma_id: id.toString(), - }, - transaction, - }); - - return Result.ok(count > 0); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * - * Actualiza el "status" de una proforma - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param id - UUID de la factura a eliminar. - * @param newStatus - nuevo estado - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - async updateStatusByIdInCompany( - companyId: UniqueID, - id: UniqueID, - newStatus: InvoiceStatus, - transaction: Transaction - ): Promise> { - try { - const [affected] = await CustomerInvoiceModel.update( - { - status: newStatus.toPrimitive(), - }, - { - where: { id: id.toPrimitive(), company_id: companyId.toPrimitive() }, - fields: ["status"], - transaction, - individualHooks: true, - } - ); - - if (affected === 0) { - return Result.fail( - new InfrastructureRepositoryError( - "Concurrency conflict or not found update customer invoice" - ) - ); - } - - return Result.ok(true); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - public async markAsIssuedByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction?: unknown - ): Promise> { - try { - const [affectedRows] = await CustomerInvoiceModel.update( - { - status: INVOICE_STATUS.ISSUED, - }, - { - where: { - id: id.toString(), - company_id: companyId.toString(), - is_proforma: true, - status: INVOICE_STATUS.APPROVED, - }, - transaction: transaction as Transaction, - } - ); - - if (affectedRows !== 1) { - return Result.fail( - new Error( - `Proforma ${id.toString()} could not be marked as issued because it is not approved or does not exist` - ) - ); - } - - return Result.ok(); - } catch (error: unknown) { - return Result.fail(error as Error); - } - } - - /** - * - * Busca una factura por su identificador único. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async getByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - const { CustomerModel } = this.database.models; - - try { - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - const mergedOptions: FindOptions> = { - ...options, - where: { - ...(options.where ?? {}), - id: id.toString(), - is_proforma: true, - company_id: companyId.toString(), - }, - order: [ - ...normalizedOrder, - [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], - ], - include: [ - ...normalizedInclude, - - { - model: CustomerModel, - as: "current_customer", - required: false, - }, - { - model: CustomerInvoiceItemModel, - as: "items", - required: false, - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - }, - { - model: CustomerInvoiceModel, - as: "linked_invoice", - required: false, - attributes: ["id"], - }, - ], - transaction, - }; - - const row = await CustomerInvoiceModel.findOne(mergedOptions); - - if (!row) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); - } - - const invoice = this.domainMapper.mapToDomain(row); - return invoice; - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - async getByIdIncludingArchivedInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - return this.getByIdInCompany(companyId, id, transaction, options); - } - - async archiveByIdInCompany( - _companyId: UniqueID, - _id: UniqueID, - _transaction: Transaction - ): Promise> { - return Result.fail(new Error("Archive is not supported by the legacy proforma repository.")); - } - - async unarchiveByIdInCompany( - _companyId: UniqueID, - _id: UniqueID, - _transaction: Transaction - ): Promise> { - return Result.fail( - new Error("Unarchive is not supported by the legacy proforma repository.") - ); - } - - /** - * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result - * - * @see Criteria - */ - /** - * Consulta proformas de una empresa usando Criteria. - * - * Responsabilidades de este método: - * - aplicar restricciones propias del repositorio/aggregate (`company_id`, `is_proforma`, `deleted_at`); - * - añadir includes necesarios para construir el read model; - * - componer la query final de Sequelize; - * - separar `findAll` y `count` para tener más control que con `findAndCountAll`. - * - * @param companyId - Identificador UUID de la empresa propietaria. - * @param criteria - Criterios de búsqueda, ordenación y paginación. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones Sequelize adicionales controladas por infraestructura. - * @returns Colección paginada de resúmenes de proforma. - * - * @see Criteria - */ - public async findByCriteriaInCompany( - companyId: UniqueID, - _scope: ProformaListScope, - criteria: Criteria, - transaction: Transaction, - options: FindOptions> = {} - ): Promise, Error>> { - const { CustomerModel } = this.database.models; - - try { - const criteriaConverter = new CriteriaToSequelizeConverter(); - - /** - * El converter solo traduce Criteria a piezas Sequelize genéricas: - * filtros, quick search, orden y paginación. - * - */ - - const criteriaQuery = criteriaConverter.convert(criteria, { - searchableFields: ["invoice_number", "reference", "description"], - mappings: { - invoice_date: { - type: "root", - column: "invoice_date", - }, - invoice_number: { - type: "root", - column: "invoice_number", - }, - reference: { - type: "root", - column: "reference", - }, - description: { - type: "root", - column: "description", - }, - recipient_name: { - type: "association", - association: "current_customer", - column: "name", - }, - }, - sortableFields: [ - "recipient_name", - "invoice_number", - "invoice_date", - "id", - "created_at", - "serie", - ], - fullTextTableAlias: "CustomerInvoiceModel", - enableFullText: true, - database: this.database, - strictMode: true, // fuerza error si ORDER BY no permitido - }); - - /** - * Restricciones propias del repositorio. - * - * El dominio `Proforma` no conoce `is_proforma`; este discriminador - * pertenece a la infraestructura porque actualmente se comparte tabla - * con facturas emitidas. - * - * No se añade `deleted_at: null` porque el modelo usa `paranoid: true`; - * Sequelize aplica automáticamente `deleted_at IS NULL` en las consultas. - */ - const baseWhere: WhereOptions> = { - is_proforma: true, - company_id: companyId.toString(), - }; - - /** - * Composición defensiva del WHERE. - * - * Se usa `Op.and` para no sobrescribir condiciones si `criteriaQuery.where` - * u `options.where` contienen operadores Sequelize. - */ - const where: WhereOptions> = { - [Op.and]: [ - baseWhere, - ...(options.where ? [options.where] : []), - ...(criteriaQuery.where ? [criteriaQuery.where] : []), - ], - }; - - /** - * Includes necesarios para materializar el resumen. - * - * `current_customer` también puede ser necesario para filtrar/ordenar por - * `recipient_name`, por eso se añade siempre. - */ - const include: Includeable[] = [ - ...normalizeInclude(options.include), - { - model: CustomerModel, - as: "current_customer", - required: false, - attributes: [ - "name", - "trade_name", - "tin", - "street", - "street2", - "city", - "postal_code", - "province", - "country", - ], - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - - /** - * Evita multiplicar filas de la query principal. - * - * Sequelize ejecuta una consulta separada para los impuestos y garantiza - * que `taxes` llegue como array. - */ - separate: true, - }, - { - model: CustomerInvoiceModel, - as: "linked_invoice", - required: false, - attributes: ["id"], - }, - ]; - - /** - * Orden final. - * - * Primero va el orden derivado de Criteria: - * - score FULLTEXT si hay quick search; - * - orderBy/orderType del cliente si viene informado. - * - * Después se añade el orden técnico recibido por `options`. - */ - const order: OrderItem[] = [ - ...normalizeOrder(criteriaQuery.order), - ...normalizeOrder(options.order), - ]; - - /** - * Query principal. - * - * `options` se expande primero para permitir opciones técnicas externas, - * pero `criteriaQuery`, `where`, `include`, `order` y `transaction` - * prevalecen explícitamente. - */ - const findQuery: FindOptions> = { - ...options, - ...criteriaQuery, - where, - include, - order, - transaction, - }; - - /** - * Query de conteo. - * - * No se incluyen asociaciones `separate`, porque no participan en el - * filtrado de filas principales y añadirían coste sin aportar precisión. - * - * `count()` no usa `FindOptions`, sino `CountOptions`; por eso `distinct` - * y `col` deben tiparse con `CountOptions`. - */ - const countQuery: CountOptions> = { - where, - include: getCountIncludes(include), - distinct: true, - col: "id", - transaction, - }; - - const [rows, count] = await Promise.all([ - CustomerInvoiceModel.findAll(findQuery), - CustomerInvoiceModel.count(countQuery), - ]); - - return this.summaryMapper.mapToReadModelCollection(rows, count); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - public async countByStatusInCompany( - companyId: UniqueID, - _scope: ProformaListScope, - criteria: Criteria, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - try { - const criteriaConverter = new CriteriaToSequelizeConverter(); - const criteriaQuery = criteriaConverter.convert(criteria, { - searchableFields: ["invoice_number", "reference", "description"], - mappings: { - invoice_date: { - type: "root", - column: "invoice_date", - }, - invoice_number: { - type: "root", - column: "invoice_number", - }, - reference: { - type: "root", - column: "reference", - }, - description: { - type: "root", - column: "description", - }, - status: { - type: "root", - column: "status", - }, - recipient_name: { - type: "association", - association: "current_customer", - column: "name", - }, - }, - sortableFields: [ - "recipient_name", - "invoice_number", - "invoice_date", - "id", - "created_at", - "serie", - ], - fullTextTableAlias: "CustomerInvoiceModel", - enableFullText: true, - database: this.database, - strictMode: true, - }); - - const where: WhereOptions> = { - [Op.and]: [ - { - is_proforma: true, - company_id: companyId.toString(), - }, - ...(options.where ? [options.where] : []), - ...(criteriaQuery.where ? [criteriaQuery.where] : []), - ], - }; - - const rows = (await CustomerInvoiceModel.findAll({ - attributes: ["status", [fn("COUNT", col("CustomerInvoiceModel.id")), "count"]], - group: ["status"], - raw: true, - transaction, - where, - })) as unknown as Array<{ status: string; count: string | number }>; - - const counts: ProformaStatusCounts = { - all: 0, - draft: 0, - sent: 0, - approved: 0, - rejected: 0, - issued: 0, - }; - - for (const row of rows) { - const value = Number(row.count); - - switch (row.status) { - case INVOICE_STATUS.DRAFT: - counts.draft = value; - break; - case INVOICE_STATUS.SENT: - counts.sent = value; - break; - case INVOICE_STATUS.APPROVED: - counts.approved = value; - break; - case INVOICE_STATUS.REJECTED: - counts.rejected = value; - break; - case INVOICE_STATUS.ISSUED: - counts.issued = value; - break; - } - } - - counts.all = - counts.draft + counts.sent + counts.approved + counts.rejected + counts.issued; - - return Result.ok(counts); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts index d678a411..f8f974be 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts @@ -1,2 +1 @@ -export * from "./sequelize-proforma-number-generator.service"; export * from "./sequelize-proforma-v2-number-generator.service"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-number-generator.service.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-number-generator.service.ts deleted file mode 100644 index 0f751219..00000000 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-number-generator.service.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import { type Maybe, Result } from "@repo/rdx-utils"; -import { type Transaction, type WhereOptions } from "sequelize"; - -import type { IProformaNumberGenerator } from "../../../../../application/proformas"; -import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain"; -import { CustomerInvoiceModel } from "../../../../common/persistence"; - -/** - * Genera numeración de proformas a nivel de empresa. - * La serie elegida en la proforma solo referencia la futura InvoiceSeries. - */ -export class SequelizeProformaNumberGenerator implements IProformaNumberGenerator { - private static readonly PREFIX = "PF-"; - private static readonly DIGITS = 4; - - public async getNextForCompany( - companyId: UniqueID, - _series: Maybe, - transaction: Transaction - ): Promise> { - const where: WhereOptions = { - company_id: companyId.toString(), - is_proforma: true, - }; - - try { - const existingInvoices = await CustomerInvoiceModel.findAll({ - attributes: ["invoice_number"], - where, - transaction, - raw: true, - lock: transaction.LOCK.UPDATE, - }); - - const maxSequence = existingInvoices.reduce((max, currentInvoice) => { - const currentSequence = this.extractSequence(currentInvoice.invoice_number); - return Math.max(max, currentSequence); - }, 0); - - const nextValue = this.formatSequence(maxSequence + 1); - - const numberResult = InvoiceNumber.create(nextValue); - if (numberResult.isFailure) { - return Result.fail(numberResult.error); - } - - return Result.ok(numberResult.data); - } catch (error) { - return Result.fail( - new Error( - `Error generating proforma number for company ${companyId}: ${(error as Error).message}` - ) - ); - } - } - - private extractSequence(invoiceNumber: string): number { - const normalized = invoiceNumber.trim().toUpperCase(); - - if (normalized.startsWith(SequelizeProformaNumberGenerator.PREFIX)) { - const suffix = normalized.slice(SequelizeProformaNumberGenerator.PREFIX.length); - const parsed = Number(suffix); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; - } - - const parsed = Number(normalized); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; - } - - private formatSequence(sequence: number): string { - const padded = String(sequence).padStart(SequelizeProformaNumberGenerator.DIGITS, "0"); - return `${SequelizeProformaNumberGenerator.PREFIX}${padded}`; - } -} diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts index 6ef470f6..f647cdfd 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/list-proformas.adapter.ts @@ -3,12 +3,11 @@ import { MoneyHelper } from "@repo/rdx-utils"; import type { ListProformasResponseDTO } from "../../../../common"; import type { ListProformasResult } from "../api"; -import { - EMPTY_PROFORMA_STATUS_COUNTS, - type ProformaList, - type ProformaListRow, - type ProformaStatus, - type ProformaStatusCounts, +import type { + ProformaList, + ProformaListRow, + ProformaStatus, + ProformaStatusCounts, } from "../entities"; /** @@ -64,7 +63,6 @@ function toSafeCount(value: unknown): number { * Adaptador para transformar los items de la API de ListProformasResult a la entidad ProformaListRow. * Reglas de adaptación: * - id, company_id, status, reference se asignan directamente. - * - is_proforma se convierte a booleano (true si es "1", false si es "0"). * * @param rowDto - item de proforma desde la API. * @returns {ProformaListRow} Objeto adaptado a ProformaListRow. diff --git a/modules/customer-invoices/templates/rodax/es/issued-invoice.frx b/modules/customer-invoices/templates/rodax/es/issued-invoice.frx index 68d27cf1..b59c6fad 100644 --- a/modules/customer-invoices/templates/rodax/es/issued-invoice.frx +++ b/modules/customer-invoices/templates/rodax/es/issued-invoice.frx @@ -28,7 +28,6 @@ namespace FastReport - diff --git a/modules/customer-invoices/templates/rodax/es/proforma.frx b/modules/customer-invoices/templates/rodax/es/proforma.frx index d4653e10..ab70007c 100755 --- a/modules/customer-invoices/templates/rodax/es/proforma.frx +++ b/modules/customer-invoices/templates/rodax/es/proforma.frx @@ -28,7 +28,6 @@ namespace FastReport - diff --git a/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts b/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts index c49e5779..3100d526 100644 --- a/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts +++ b/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts @@ -42,7 +42,6 @@ export const CreateProformaFromFactugesRequestSchema = z.object({ //id: z.uuid(), factuges_id: z.string(), //company_id: z.uuid(), - //is_proforma: z.string().default("1"), //status: z.string(), series: z.string(),