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:
parent
ee040b8335
commit
92b17ba307
@ -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
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
@ -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 => {
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from "./issued-invoice.repository";
|
||||
export * from "./issued-invoice-v2.repository";
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import type { IProformaNumberGenerator } from "../../../application";
|
||||
import { SequelizeProformaNumberGenerator } from "../persistence";
|
||||
|
||||
export const buildLegacyProformaNumberGenerator = (): IProformaNumberGenerator =>
|
||||
new SequelizeProformaNumberGenerator();
|
||||
|
||||
export const buildProformaNumberGenerator = buildLegacyProformaNumberGenerator;
|
||||
@ -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;
|
||||
@ -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 => {
|
||||
|
||||
@ -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;
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from "./proforma.repository";
|
||||
export * from "./proforma-v2.repository";
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from "./sequelize-proforma-number-generator.service";
|
||||
export * from "./sequelize-proforma-v2-number-generator.service";
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user