Uecko_ERP/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts

336 lines
9.8 KiB
TypeScript
Raw Normal View History

2025-10-03 19:01:38 +00:00
import {
EntityNotFoundError,
InfrastructureRepositoryError,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
2025-10-30 18:26:57 +00:00
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria";
2025-06-11 15:13:44 +00:00
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
2025-09-13 18:45:55 +00:00
import { Transaction } from "sequelize";
2025-06-12 06:55:17 +00:00
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
2025-09-11 17:52:50 +00:00
import {
CustomerInvoiceListDTO,
2025-09-13 18:45:55 +00:00
ICustomerInvoiceDomainMapper,
2025-09-11 17:52:50 +00:00
ICustomerInvoiceListMapper,
} from "../mappers";
2025-09-13 18:45:55 +00:00
import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.model";
import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model";
import { CustomerInvoiceTaxModel } from "./models/customer-invoice-tax.model";
import { CustomerInvoiceModel } from "./models/customer-invoice.model";
2025-06-11 15:13:44 +00:00
2025-06-12 06:55:17 +00:00
export class CustomerInvoiceRepository
extends SequelizeRepository<CustomerInvoice>
implements ICustomerInvoiceRepository
{
2025-08-23 11:57:48 +00:00
// Listado por tenant con criteria saneada
/* async searchInCompany(criteria: any, companyId: string): Promise<{
rows: InvoiceListRow[];
total: number;
limit: number;
offset: number;
}> {
const { where, order, limit, offset, attributes } = sanitizeListCriteria(criteria);
// WHERE con scope de company
const scopedWhere = { ...where, company_id: companyId };
const options: FindAndCountOptions = {
where: scopedWhere,
order,
limit,
offset,
attributes,
raw: true, // devolvemos objetos planos -> más rápido
nest: false,
distinct: true // por si en el futuro añadimos includes no duplicar count
};
const { rows, count } = await CustomerInvoiceModel.findAndCountAll(options);
return {
rows: rows as unknown as InvoiceListRow[],
total: typeof count === "number" ? count : (count as any[]).length,
limit,
offset,
};
} */
2025-06-26 11:32:55 +00:00
/**
*
2025-10-03 19:01:38 +00:00
* Crea una nueva factura.
2025-06-26 11:32:55 +00:00
*
* @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para la operación.
2025-10-03 19:01:38 +00:00
* @returns Result<void, Error>
2025-06-26 11:32:55 +00:00
*/
2025-10-03 19:01:38 +00:00
async create(invoice: CustomerInvoice, transaction?: Transaction): Promise<Result<void, Error>> {
2025-06-26 11:32:55 +00:00
try {
2025-09-13 18:45:55 +00:00
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
resource: "customer-invoice",
});
2025-10-03 19:01:38 +00:00
const dto = mapper.mapToPersistence(invoice);
2025-09-11 12:05:50 +00:00
2025-10-03 19:01:38 +00:00
if (dto.isFailure) {
return Result.fail(dto.error);
2025-09-11 12:05:50 +00:00
}
2025-10-03 19:01:38 +00:00
const { data } = dto;
await CustomerInvoiceModel.create(data, {
include: [{ all: true }],
transaction,
});
2025-09-11 12:05:50 +00:00
2025-10-03 19:01:38 +00:00
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(invoice: CustomerInvoice, transaction?: Transaction): Promise<Result<void, Error>> {
try {
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
resource: "customer-invoice",
});
const dto = mapper.mapToPersistence(invoice);
const { id, ...updatePayload } = dto.data;
const [affected] = await CustomerInvoiceModel.update(updatePayload, {
where: { id /*, version */ },
//fields: Object.keys(updatePayload),
transaction,
individualHooks: true,
});
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError(
"Concurrency conflict or not found update customer invoice"
)
);
}
return Result.ok();
2025-06-26 11:32:55 +00:00
} catch (err: unknown) {
2025-08-26 18:55:59 +00:00
return Result.fail(translateSequelizeError(err));
2025-06-26 11:32:55 +00:00
}
2025-06-11 15:13:44 +00:00
}
2025-09-03 10:41:12 +00:00
/**
* 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.
* @returns Result<boolean, Error>
*/
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await CustomerInvoiceModel.count({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok(Boolean(count > 0));
2025-09-11 12:05:50 +00:00
} catch (error: unknown) {
2025-09-03 10:41:12 +00:00
return Result.fail(translateSequelizeError(error));
}
}
2025-06-26 11:32:55 +00:00
/**
*
* Busca una factura por su identificador único.
2025-09-03 10:41:12 +00:00
*
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
2025-06-26 11:32:55 +00:00
* @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/
2025-09-03 10:41:12 +00:00
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
2025-10-04 16:29:14 +00:00
const { CustomerModel } = this._database.models;
2025-06-11 15:13:44 +00:00
try {
2025-09-14 08:18:02 +00:00
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
resource: "customer-invoice",
});
2025-09-03 10:41:12 +00:00
const row = await CustomerInvoiceModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
2025-09-17 17:37:41 +00:00
order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]],
2025-09-10 10:27:07 +00:00
include: [
{
model: CustomerModel,
as: "current_customer",
required: false, // false => LEFT JOIN
},
2025-09-10 16:06:29 +00:00
{
model: CustomerInvoiceItemModel,
as: "items",
required: false,
include: [
{
model: CustomerInvoiceItemTaxModel,
as: "taxes",
required: false,
},
],
},
{
model: CustomerInvoiceTaxModel,
as: "taxes",
required: false,
},
2025-09-10 10:27:07 +00:00
],
2025-09-03 10:41:12 +00:00
transaction,
});
2025-06-26 11:32:55 +00:00
2025-09-03 10:41:12 +00:00
if (!row) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
2025-06-26 11:32:55 +00:00
}
2025-06-11 15:13:44 +00:00
2025-10-04 16:29:14 +00:00
const invoice = mapper.mapToDomain(row);
return invoice;
2025-06-26 11:32:55 +00:00
} catch (err: unknown) {
2025-08-26 18:55:59 +00:00
return Result.fail(translateSequelizeError(err));
2025-06-11 15:13:44 +00:00
}
}
2025-06-26 11:32:55 +00:00
/**
*
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
2025-09-03 10:41:12 +00:00
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
2025-06-26 11:32:55 +00:00
* @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice[], Error>
*
* @see Criteria
*/
2025-09-03 10:41:12 +00:00
public async findByCriteriaInCompany(
companyId: UniqueID,
2025-06-11 15:13:44 +00:00
criteria: Criteria,
2025-06-26 11:32:55 +00:00
transaction: Transaction
2025-09-11 17:52:50 +00:00
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
2025-10-04 16:29:14 +00:00
const { CustomerModel } = this._database.models;
2025-06-11 15:13:44 +00:00
try {
2025-09-14 08:18:02 +00:00
const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({
resource: "customer-invoice",
query: "LIST",
});
2025-10-04 16:29:14 +00:00
2025-06-26 11:32:55 +00:00
const converter = new CriteriaToSequelizeConverter();
2025-10-24 11:01:51 +00:00
const query = converter.convert(criteria, {
searchableFields: ["invoice_number", "reference", "description"],
mappings: {
reference: "CustomerInvoiceModel.reference",
},
allowedFields: ["invoice_date", "id", "created_at"],
enableFullText: true,
database: this._database,
strictMode: true, // fuerza error si ORDER BY no permitido
});
2025-06-26 11:32:55 +00:00
2025-09-03 10:41:12 +00:00
query.where = {
...query.where,
company_id: companyId.toString(),
2025-10-24 11:01:51 +00:00
deleted_at: null,
2025-09-03 10:41:12 +00:00
};
2025-09-09 15:48:12 +00:00
query.include = [
{
model: CustomerModel,
2025-09-09 18:13:54 +00:00
as: "current_customer",
2025-09-09 15:48:12 +00:00
required: false, // false => LEFT JOIN
2025-10-24 11:01:51 +00:00
attributes: [
"name",
"trade_name",
"tin",
"street",
"street2",
"city",
"postal_code",
"province",
"country",
],
2025-09-09 15:48:12 +00:00
},
2025-09-10 18:14:19 +00:00
{
model: CustomerInvoiceTaxModel,
as: "taxes",
required: false,
2025-10-24 11:01:51 +00:00
separate: true, // => query aparte, devuelve siempre array
attributes: ["tax_id", "tax_code"],
2025-09-10 18:14:19 +00:00
},
2025-09-09 15:48:12 +00:00
];
2025-10-24 11:01:51 +00:00
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
/*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({
2025-06-26 11:32:55 +00:00
...query,
2025-06-11 15:13:44 +00:00
transaction,
2025-10-24 11:01:51 +00:00
});*/
const [rows, count] = await Promise.all([
CustomerInvoiceModel.findAll({
...query,
transaction,
}),
CustomerInvoiceModel.count({
where: query.where,
distinct: true, // evita duplicados por LEFT JOIN
transaction,
}),
]);
2025-06-11 15:13:44 +00:00
2025-09-11 15:14:51 +00:00
return mapper.mapToDTOCollection(rows, count);
2025-06-26 11:32:55 +00:00
} catch (err: unknown) {
2025-08-26 18:55:59 +00:00
return Result.fail(translateSequelizeError(err));
2025-06-11 15:13:44 +00:00
}
}
2025-06-26 11:32:55 +00:00
/**
*
* Elimina o marca como eliminada una factura.
2025-09-03 10:41:12 +00:00
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
2025-06-26 11:32:55 +00:00
* @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación.
2025-10-04 16:29:14 +00:00
* @returns Result<boolean, Error>
2025-06-26 11:32:55 +00:00
*/
2025-09-03 10:41:12 +00:00
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
2025-10-03 19:01:38 +00:00
transaction: Transaction
2025-10-04 16:29:14 +00:00
): Promise<Result<boolean, Error>> {
2025-06-11 15:13:44 +00:00
try {
2025-09-03 10:41:12 +00:00
const deleted = await CustomerInvoiceModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
2025-10-04 16:29:14 +00:00
if (deleted === 0) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
}
return Result.ok(true);
2025-06-26 11:32:55 +00:00
} catch (err: unknown) {
2025-08-26 18:55:59 +00:00
return Result.fail(translateSequelizeError(err));
2025-06-11 15:13:44 +00:00
}
}
}