refactor: eliminar mapeadores, repositorios y servicios heredados de proformas

* Se eliminaron `SequelizeProformaTaxesDomainMapper` y los archivos relacionados.
* Se eliminaron `SequelizeInvoiceRecipientSummaryMapper` y `SequelizeProformaSummaryMapper`.
* Se eliminó `ProformaRepository` y sus métodos asociados.
* Se limpiaron los servicios sin uso, incluido `SequelizeProformaNumberGenerator`.
* Se actualizaron las importaciones y exportaciones de los módulos relacionados para reflejar la eliminación del código heredado.
* Se ajustaron las plantillas para eliminar las referencias a `is_proforma`.
* Se modificó el DTO de la solicitud para excluir el campo `is_proforma`.
This commit is contained in:
David Arranz 2026-08-04 12:09:19 +02:00
parent ee040b8335
commit 92b17ba307
45 changed files with 13 additions and 5745 deletions

View File

@ -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

View File

@ -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`

View File

@ -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()),

View File

@ -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;

View File

@ -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",
}

View File

@ -1,9 +0,0 @@
import { CompositeSpecification } from "@repo/rdx-ddd";
import type { CustomerInvoice } from "../aggregates";
export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification<CustomerInvoice> {
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
return proforma.isProforma;
}
}

View File

@ -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";

View File

@ -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,

View File

@ -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<CustomerInvoiceItemModel>,
InferCreationAttributes<CustomerInvoiceItemModel, { omit: "invoice" }>
> {
declare item_id: string;
declare invoice_id: string;
declare position: number;
declare description: CreationOptional<string | null>;
declare quantity_value: CreationOptional<number | null>;
declare quantity_scale: number;
declare unit_amount_value: CreationOptional<number | null>;
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<number | null>;
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<string | null>;
declare iva_percentage_value: CreationOptional<number | null>;
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<string | null>;
declare rec_percentage_value: CreationOptional<number | null>;
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<string | null>;
declare retention_percentage_value: CreationOptional<number | null>;
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<CustomerInvoiceModel>;
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;
};

View File

@ -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<CustomerInvoiceTaxModel>,
InferCreationAttributes<CustomerInvoiceTaxModel>
> {
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<string | null>;
declare iva_percentage_value: CreationOptional<number | null>;
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<string | null>;
declare rec_percentage_value: CreationOptional<number | null>;
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<string | null>;
declare retention_percentage_value: CreationOptional<number | null>;
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<Proforma>;
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;
};

View File

@ -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<CustomerInvoiceModel>,
InferCreationAttributes<CustomerInvoiceModel, { omit: "items" | "taxes" }>
> {
// Version
// declare version: CreationOptional<number>;
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<string | null>; // ID de la proforma origen de la factura
declare series: CreationOptional<string | null>;
declare invoice_number: CreationOptional<string>;
declare invoice_date: CreationOptional<string>;
declare operation_date: CreationOptional<string | null>;
declare language_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>;
declare reference: CreationOptional<string | null>;
declare description: CreationOptional<string | null>;
declare notes: CreationOptional<string | null>;
// Método de pago
declare payment_method_id: CreationOptional<string | null>;
declare payment_method_description: CreationOptional<string | null>;
// Tax regime
declare tax_regime_code: CreationOptional<string | null>;
declare tax_regime_description: CreationOptional<string | null>;
// 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<string | null>;
declare customer_name: CreationOptional<string | null>;
declare customer_street: CreationOptional<string | null>;
declare customer_street2: CreationOptional<string | null>;
declare customer_city: CreationOptional<string | null>;
declare customer_province: CreationOptional<string | null>;
declare customer_postal_code: CreationOptional<string | null>;
declare customer_country: CreationOptional<string | null>;
// Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
declare current_customer?: NonAttribute<CustomerModel | null>;
declare verifactu?: NonAttribute<VerifactuRecordModel | null>;
declare proforma?: NonAttribute<CustomerInvoiceModel | null>;
declare linked_invoice?: NonAttribute<CustomerInvoiceModel | null>;
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;
};

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -12,33 +12,12 @@ type BuildIssuedInvoicePersistenceParams = {
database: Sequelize;
};
/*export type LegacyIssuedInvoicePersistence = {
mode: "legacy";
mappers: IIssuedInvoicePersistenceMappers;
repository: ReturnType<typeof buildLegacyIssuedInvoiceRepository>;
};*/
export type V2IssuedInvoicePersistence = {
mode: "v2";
mappers: IIssuedInvoicePersistenceMappersV2;
repository: ReturnType<typeof buildIssuedInvoiceRepositoryV2>;
};
/*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 => {

View File

@ -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;

View File

@ -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<IssuedInvoice, Error> {
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<CustomerInvoiceCreationAttributes, Error> {
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<CustomerInvoiceCreationAttributes> = {
// 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<CustomerInvoiceCreationAttributes>(
invoiceValues as CustomerInvoiceCreationAttributes
);
}
}

View File

@ -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<IIssuedInvoiceItemCreateProps> & { itemId?: UniqueID } {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
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<IssuedInvoiceItem, Error> {
const { errors, index } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
// 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<CustomerInvoiceItemCreationAttributes, Error> {
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,
});
}
}

View File

@ -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<IssuedInvoiceRecipient, Error> {
/**
* - Issued invoice -> snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
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()),
};
}
}

View File

@ -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<IssuedInvoiceTax, Error> {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
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<CustomerInvoiceTaxCreationAttributes, Error> {
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);
}
}
}

View File

@ -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<IssuedInvoiceRecipient, Error> {
/**
* - Issued invoice => snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceSummary>;
};
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);
}
}

View File

@ -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<IssuedInvoiceSummary, Error> {
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<VerifactuRecord> = 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,
};
}
}

View File

@ -1,2 +1 @@
export * from "./issued-invoice.repository";
export * from "./issued-invoice-v2.repository";

View File

@ -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<IssuedInvoice>
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<void, Error>
*/
async create(invoice: IssuedInvoice, transaction?: Transaction): Promise<Result<void, Error>> {
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<boolean, Error>
*/
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<boolean, Error>> {
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<CustomerInvoice, Error>
*/
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<IssuedInvoice, Error>> {
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<InferAttributes<CustomerInvoiceModel>> = {
...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<CustomerInvoice[], Error>
*
* @see Criteria
*/
public async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Collection<IssuedInvoiceSummary>, 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<InferAttributes<CustomerInvoiceModel>> = {
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<InferAttributes<CustomerInvoiceModel>> = {
[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<InferAttributes<CustomerInvoiceModel>> = {
...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<InferAttributes<CustomerInvoiceModel>> = {
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));
}
}
}

View File

@ -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";

View File

@ -1,7 +0,0 @@
import type { IProformaNumberGenerator } from "../../../application";
import { SequelizeProformaNumberGenerator } from "../persistence";
export const buildLegacyProformaNumberGenerator = (): IProformaNumberGenerator =>
new SequelizeProformaNumberGenerator();
export const buildProformaNumberGenerator = buildLegacyProformaNumberGenerator;

View File

@ -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;

View File

@ -13,13 +13,6 @@ type BuildProformaPersistenceParams = {
documentSeriesServices: DocumentSeriesPublicServicesType;
};
/*export type LegacyProformaPersistence = {
mode: "legacy";
mappers: IProformaPersistenceMappers;
repository: ReturnType<typeof buildLegacyProformaRepository>;
numberGenerator: ReturnType<typeof buildLegacyProformaNumberGenerator>;
};*/
export type V2ProformaPersistence = {
mode: "v2";
mappers: IProformaPersistenceMappersV2;
@ -27,22 +20,6 @@ export type V2ProformaPersistence = {
numberGenerator: ReturnType<typeof buildV2ProformaNumberGenerator>;
};
/*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 => {

View File

@ -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;

View File

@ -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";

View File

@ -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<InvoicePaymentMethod>();
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<Proforma, Error> {
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<CustomerInvoiceCreationAttributes, Error> {
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<CustomerInvoiceCreationAttributes>(
proformaValues satisfies CustomerInvoiceCreationAttributes
);
}
}

View File

@ -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<IProformaItemCreateProps & ProformaItemTaxesProps> & { itemId?: UniqueID } {
const { errors, index, parent } = params as {
index: number;
errors: ValidationErrorDetail[];
parent: Partial<IProformaCreateProps>;
};
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<Maybe<Tax>, 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<ProformaItem, Error> {
const { errors } = params as {
index: number;
errors: ValidationErrorDetail[];
parent: Partial<IProformaCreateProps>;
};
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<CustomerInvoiceItemCreationAttributes, Error> {
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,
});
}
}

View File

@ -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<Maybe<ProformaRecipient>, Error> {
/**
* En proforma -> datos de "current_customer"
*/
const { errors, parent } = params as {
errors: ValidationErrorDetail[];
parent: Partial<IProformaCreateProps>;
};
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<ProformaRecipient>, 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,
};
}
}

View File

@ -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<IProformaTaxTotals, Error> {
throw new Error("Se calcula a partir de las líneas de detalle");
}
public mapToPersistence(
source: IProformaTaxTotals,
params?: MapperParamsType
): Result<CustomerInvoiceTaxCreationAttributes, Error> {
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);
}
}
}

View File

@ -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";

View File

@ -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<ProformaRecipient, Error> {
/**
* En proforma -> datos de "current_customer"
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<ProformaSummary>;
};
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);
}
}

View File

@ -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<ProformaSummary, Error> {
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,
};
}
}

View File

@ -1,2 +1 @@
export * from "./proforma.repository";
export * from "./proforma-v2.repository";

View File

@ -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<Proforma>
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<void, Error>
*/
async create(proforma: Proforma, transaction?: Transaction): Promise<Result<void, Error>> {
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<void, Error>
*/
async update(proforma: Proforma, transaction: Transaction): Promise<Result<void, Error>> {
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<boolean, Error>
*/
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<boolean, Error>> {
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<void, Error>
*/
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
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<Result<boolean, Error>> {
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<boolean, Error>
*/
async updateStatusByIdInCompany(
companyId: UniqueID,
id: UniqueID,
newStatus: InvoiceStatus,
transaction: Transaction
): Promise<Result<boolean, Error>> {
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<Result<void, Error>> {
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<CustomerInvoice, Error>
*/
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Proforma, Error>> {
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<InferAttributes<CustomerInvoiceModel>> = {
...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<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Proforma, Error>> {
return this.getByIdInCompany(companyId, id, transaction, options);
}
async archiveByIdInCompany(
_companyId: UniqueID,
_id: UniqueID,
_transaction: Transaction
): Promise<Result<boolean, Error>> {
return Result.fail(new Error("Archive is not supported by the legacy proforma repository."));
}
async unarchiveByIdInCompany(
_companyId: UniqueID,
_id: UniqueID,
_transaction: Transaction
): Promise<Result<boolean, Error>> {
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<CustomerInvoice[], Error>
*
* @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<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Collection<ProformaSummary>, 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<InferAttributes<CustomerInvoiceModel>> = {
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<InferAttributes<CustomerInvoiceModel>> = {
[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<InferAttributes<CustomerInvoiceModel>> = {
...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<InferAttributes<CustomerInvoiceModel>> = {
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<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<ProformaStatusCounts, Error>> {
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<InferAttributes<CustomerInvoiceModel>> = {
[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));
}
}
}

View File

@ -1,2 +1 @@
export * from "./sequelize-proforma-number-generator.service";
export * from "./sequelize-proforma-v2-number-generator.service";

View File

@ -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<InvoiceSerie>,
transaction: Transaction
): Promise<Result<InvoiceNumber, Error>> {
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}`;
}
}

View File

@ -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.

View File

@ -28,7 +28,6 @@ namespace FastReport
<Column Name="item" Alias="invoice" DataType="FastReport.JsonBase">
<Column Name="id" DataType="System.String"/>
<Column Name="company_id" DataType="System.String"/>
<Column Name="is_proforma" DataType="System.String"/>
<Column Name="invoice_number" DataType="System.String"/>
<Column Name="status" DataType="System.String"/>
<Column Name="series" DataType="System.String"/>

View File

@ -28,7 +28,6 @@ namespace FastReport
<Column Name="item" Alias="invoice" DataType="FastReport.JsonBase">
<Column Name="id" DataType="System.String"/>
<Column Name="company_id" DataType="System.String"/>
<Column Name="is_proforma" DataType="System.String"/>
<Column Name="invoice_number" DataType="System.String"/>
<Column Name="status" DataType="System.String"/>
<Column Name="series" DataType="System.String"/>

View File

@ -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(),