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`.
|
> 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`.
|
> 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
|
## Decisión consolidada
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
Este directorio mezcla SQL vigentes de soporte con SQL historicos/transicionales.
|
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
|
## SQL vigentes
|
||||||
|
|
||||||
- `add-proforma-document-series-columns.sql`
|
- `add-proforma-document-series-columns.sql`
|
||||||
|
|||||||
@ -40,7 +40,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
|||||||
return {
|
return {
|
||||||
id: invoice.id.toString(),
|
id: invoice.id.toString(),
|
||||||
company_id: invoice.companyId.toString(),
|
company_id: invoice.companyId.toString(),
|
||||||
is_proforma: false,
|
|
||||||
|
|
||||||
document_series_id: maybeToNullable(invoice.documentSeriesId, (value) => value),
|
document_series_id: maybeToNullable(invoice.documentSeriesId, (value) => value),
|
||||||
invoice_series_code: maybeToNullable(Maybe.some(invoice.series), (value) => value.toString()),
|
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 {
|
export interface IIssuedInvoiceFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
is_proforma: boolean;
|
|
||||||
|
|
||||||
document_series_id: string | null;
|
document_series_id: string | null;
|
||||||
invoice_series_code: string | null;
|
invoice_series_code: string | null;
|
||||||
|
|||||||
@ -11,8 +11,8 @@ export enum INVOICE_STATUS {
|
|||||||
APPROVED = "approved", // <- Proforma
|
APPROVED = "approved", // <- Proforma
|
||||||
REJECTED = "rejected", // <- Proforma
|
REJECTED = "rejected", // <- Proforma
|
||||||
|
|
||||||
// status === "issued" <- (si is_proforma === true) => Es una proforma (histórica)
|
// `issued` se conserva como estado final compartido tras el split histórico
|
||||||
// status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
// entre proformas e issued invoices.
|
||||||
ISSUED = "issued",
|
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 "./proforma-can-transtion-to-issued.specification";
|
||||||
export * from "./status-invoice-is-draft.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 issuedInvoiceModelInit from "./models/sequelize-issued-invoice.model";
|
||||||
import issuedInvoiceItemModelInit from "./models/sequelize-issued-invoice-item.model";
|
import issuedInvoiceItemModelInit from "./models/sequelize-issued-invoice-item.model";
|
||||||
import issuedInvoiceTaxModelInit from "./models/sequelize-issued-invoice-tax.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.
|
// Array de inicializadores para que registerModels() lo use.
|
||||||
export const models = [
|
export const models = [
|
||||||
customerInvoiceModelInit,
|
|
||||||
customerInvoiceItemModelInit,
|
|
||||||
customerInvoiceTaxesModelInit,
|
|
||||||
proformaModelInit,
|
proformaModelInit,
|
||||||
proformaItemModelInit,
|
proformaItemModelInit,
|
||||||
proformaTaxModelInit,
|
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.model";
|
||||||
export * from "./sequelize-proforma-item.model";
|
export * from "./sequelize-proforma-item.model";
|
||||||
export * from "./sequelize-proforma-tax.model";
|
export * from "./sequelize-proforma-tax.model";
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
export * from "./issued-invoice-persistence.di";
|
export * from "./issued-invoice-persistence.di";
|
||||||
export * from "./issued-invoice-persistence-mappers-v2.di";
|
export * from "./issued-invoice-persistence-mappers-v2.di";
|
||||||
export * from "./issued-invoice-public-services";
|
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-v2.di";
|
||||||
//export * from "./issued-invoice-repositories.di";
|
|
||||||
export * from "./issued-invoices.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;
|
database: Sequelize;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*export type LegacyIssuedInvoicePersistence = {
|
|
||||||
mode: "legacy";
|
|
||||||
mappers: IIssuedInvoicePersistenceMappers;
|
|
||||||
repository: ReturnType<typeof buildLegacyIssuedInvoiceRepository>;
|
|
||||||
};*/
|
|
||||||
|
|
||||||
export type V2IssuedInvoicePersistence = {
|
export type V2IssuedInvoicePersistence = {
|
||||||
mode: "v2";
|
mode: "v2";
|
||||||
mappers: IIssuedInvoicePersistenceMappersV2;
|
mappers: IIssuedInvoicePersistenceMappersV2;
|
||||||
repository: ReturnType<typeof buildIssuedInvoiceRepositoryV2>;
|
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 = (
|
export const buildV2IssuedInvoicePersistence = (
|
||||||
params: BuildIssuedInvoicePersistenceParams
|
params: BuildIssuedInvoicePersistenceParams
|
||||||
): V2IssuedInvoicePersistence => {
|
): 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";
|
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-public-services";
|
||||||
export * from "./proforma-persistence.di";
|
export * from "./proforma-persistence.di";
|
||||||
export * from "./proforma-persistence-mappers-v2.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-v2.di";
|
||||||
export * from "./proforma-number-generator.di";
|
|
||||||
export * from "./proforma-repositories-v2.di";
|
export * from "./proforma-repositories-v2.di";
|
||||||
export * from "./proforma-repositories.di";
|
|
||||||
export * from "./proformas.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;
|
documentSeriesServices: DocumentSeriesPublicServicesType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*export type LegacyProformaPersistence = {
|
|
||||||
mode: "legacy";
|
|
||||||
mappers: IProformaPersistenceMappers;
|
|
||||||
repository: ReturnType<typeof buildLegacyProformaRepository>;
|
|
||||||
numberGenerator: ReturnType<typeof buildLegacyProformaNumberGenerator>;
|
|
||||||
};*/
|
|
||||||
|
|
||||||
export type V2ProformaPersistence = {
|
export type V2ProformaPersistence = {
|
||||||
mode: "v2";
|
mode: "v2";
|
||||||
mappers: IProformaPersistenceMappersV2;
|
mappers: IProformaPersistenceMappersV2;
|
||||||
@ -27,22 +20,6 @@ export type V2ProformaPersistence = {
|
|||||||
numberGenerator: ReturnType<typeof buildV2ProformaNumberGenerator>;
|
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 = (
|
export const buildV2ProformaPersistence = (
|
||||||
params: BuildProformaPersistenceParams
|
params: BuildProformaPersistenceParams
|
||||||
): V2ProformaPersistence => {
|
): 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-domain.mapper";
|
||||||
export * from "./sequelize-proforma-v2-item-domain.mapper";
|
export * from "./sequelize-proforma-v2-item-domain.mapper";
|
||||||
export * from "./sequelize-proforma-v2-recipient-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-summary.mapper";
|
||||||
export * from "./sequelize-proforma-v2-recipient-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";
|
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";
|
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 { ListProformasResponseDTO } from "../../../../common";
|
||||||
import type { ListProformasResult } from "../api";
|
import type { ListProformasResult } from "../api";
|
||||||
import {
|
import type {
|
||||||
EMPTY_PROFORMA_STATUS_COUNTS,
|
ProformaList,
|
||||||
type ProformaList,
|
ProformaListRow,
|
||||||
type ProformaListRow,
|
ProformaStatus,
|
||||||
type ProformaStatus,
|
ProformaStatusCounts,
|
||||||
type ProformaStatusCounts,
|
|
||||||
} from "../entities";
|
} 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.
|
* Adaptador para transformar los items de la API de ListProformasResult a la entidad ProformaListRow.
|
||||||
* Reglas de adaptación:
|
* Reglas de adaptación:
|
||||||
* - id, company_id, status, reference se asignan directamente.
|
* - 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.
|
* @param rowDto - item de proforma desde la API.
|
||||||
* @returns {ProformaListRow} Objeto adaptado a ProformaListRow.
|
* @returns {ProformaListRow} Objeto adaptado a ProformaListRow.
|
||||||
|
|||||||
@ -28,7 +28,6 @@ namespace FastReport
|
|||||||
<Column Name="item" Alias="invoice" DataType="FastReport.JsonBase">
|
<Column Name="item" Alias="invoice" DataType="FastReport.JsonBase">
|
||||||
<Column Name="id" DataType="System.String"/>
|
<Column Name="id" DataType="System.String"/>
|
||||||
<Column Name="company_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="invoice_number" DataType="System.String"/>
|
||||||
<Column Name="status" DataType="System.String"/>
|
<Column Name="status" DataType="System.String"/>
|
||||||
<Column Name="series" 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="item" Alias="invoice" DataType="FastReport.JsonBase">
|
||||||
<Column Name="id" DataType="System.String"/>
|
<Column Name="id" DataType="System.String"/>
|
||||||
<Column Name="company_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="invoice_number" DataType="System.String"/>
|
||||||
<Column Name="status" DataType="System.String"/>
|
<Column Name="status" DataType="System.String"/>
|
||||||
<Column Name="series" DataType="System.String"/>
|
<Column Name="series" DataType="System.String"/>
|
||||||
|
|||||||
@ -42,7 +42,6 @@ export const CreateProformaFromFactugesRequestSchema = z.object({
|
|||||||
//id: z.uuid(),
|
//id: z.uuid(),
|
||||||
factuges_id: z.string(),
|
factuges_id: z.string(),
|
||||||
//company_id: z.uuid(),
|
//company_id: z.uuid(),
|
||||||
//is_proforma: z.string().default("1"),
|
|
||||||
|
|
||||||
//status: z.string(),
|
//status: z.string(),
|
||||||
series: z.string(),
|
series: z.string(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user