Proformas => búsquedas rápidas

This commit is contained in:
David Arranz 2026-08-06 13:27:36 +02:00
parent 26e2ee59bd
commit e8f40b1495
3 changed files with 144 additions and 2 deletions

View File

@ -0,0 +1,70 @@
import test from "node:test";
import assert from "node:assert/strict";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { Op, Sequelize, type WhereOptions } from "sequelize";
import { buildProformaQuickSearchWhere } from "./proforma-quick-search.ts";
function buildWhereSql(quickSearch: string): string {
const database = new Sequelize("mysql://root:pass@localhost:3306/erp_test", {
logging: false,
});
const criteria = Criteria.fromPrimitives([], "proforma_date", "DESC", 25, 0, quickSearch);
const criteriaConverter = new CriteriaToSequelizeConverter();
const criteriaQuery = criteriaConverter.convert(criteria, {
searchableFields: ["proforma_reference", "reference", "description"],
fullTextTableAlias: "ProformaModel",
enableFullText: true,
database,
strictMode: true,
});
const where = {
[Op.and]: [
{ company_id: "company-1" },
buildProformaQuickSearchWhere({
quickSearch,
fullTextWhere: criteriaQuery.where as WhereOptions,
}),
],
};
return database.getQueryInterface().queryGenerator.whereQuery(where);
}
test("genera fallback LIKE para referencia documental con guion", () => {
const sql = buildWhereSql("PF-0066");
assert.match(sql, /MATCH\(`ProformaModel`\.`proforma_reference`, `ProformaModel`\.`reference`, `ProformaModel`\.`description`\)/);
assert.match(sql, /\+PF\* \+0066\*/);
assert.match(sql, /`proforma_reference` LIKE '%PF-0066%'/);
assert.match(sql, /`proforma_number` LIKE '%PF-0066%'/);
});
test("incluye el TIN del current_customer en quick search", () => {
const sql = buildWhereSql("B12345678");
assert.match(sql, /`current_customer`\.`tin` LIKE '%B12345678%'/);
});
test("incluye nombre y trade name del current_customer en quick search", () => {
const sql = buildWhereSql("Acme");
assert.match(sql, /`current_customer`\.`name` LIKE '%Acme%'/);
assert.match(sql, /`current_customer`\.`trade_name` LIKE '%Acme%'/);
});
test("acepta operadores reservados de MariaDB sin generar SQL inválida", () => {
const sql = buildWhereSql('+"PF-0066"(test)');
assert.equal(typeof sql, "string");
assert.ok(sql.length > 0);
assert.match(sql, /\+PF\* \+0066\* \+test\*/);
assert.match(sql, /LIKE '%\+"PF-0066"\(test\)%'/);
});
test("mantiene el aislamiento por empresa en la cláusula final", () => {
const sql = buildWhereSql("PF-0066");
assert.match(sql, /`company_id` = 'company-1'/);
});

View File

@ -0,0 +1,55 @@
import type { ProformaModel } from "../../../../common";
import {
Op,
Sequelize,
type InferAttributes,
type WhereOptions,
} from "sequelize";
type BuildProformaQuickSearchWhereParams = {
quickSearch?: string;
fullTextWhere?: WhereOptions<InferAttributes<ProformaModel>>;
customerAssociationAlias?: string;
};
/**
* Añade condiciones explícitas para campos que no quedan bien cubiertos por FULLTEXT.
*
* Mantiene el MATCH ... AGAINST existente para las columnas indexadas, pero agrega
* búsquedas `LIKE` sobre la referencia visible y el recipient del listado.
*/
export function buildProformaQuickSearchWhere(
params: BuildProformaQuickSearchWhereParams
): WhereOptions<InferAttributes<ProformaModel>> | undefined {
const term = params.quickSearch?.trim();
if (!term) {
return params.fullTextWhere;
}
const likePattern = `%${term}%`;
const customerAssociationAlias = params.customerAssociationAlias ?? "current_customer";
const clauses: WhereOptions<InferAttributes<ProformaModel>>[] = [
{ proforma_reference: { [Op.like]: likePattern } },
{ proforma_number: { [Op.like]: likePattern } },
{ reference: { [Op.like]: likePattern } },
{ description: { [Op.like]: likePattern } },
Sequelize.where(Sequelize.col(`${customerAssociationAlias}.name`), {
[Op.like]: likePattern,
}) as unknown as WhereOptions<InferAttributes<ProformaModel>>,
Sequelize.where(Sequelize.col(`${customerAssociationAlias}.trade_name`), {
[Op.like]: likePattern,
}) as unknown as WhereOptions<InferAttributes<ProformaModel>>,
Sequelize.where(Sequelize.col(`${customerAssociationAlias}.tin`), {
[Op.like]: likePattern,
}) as unknown as WhereOptions<InferAttributes<ProformaModel>>,
];
if (params.fullTextWhere) {
clauses.unshift(params.fullTextWhere);
}
return { [Op.or]: clauses };
}

View File

@ -41,6 +41,7 @@ import {
ProformaTaxModel,
} from "../../../../common";
import type { SequelizeProformaV2DomainMapper, SequelizeProformaV2SummaryMapper } from "../mappers";
import { buildProformaQuickSearchWhere } from "./proforma-quick-search";
export class SequelizeProformaRepositoryV2
extends SequelizeRepository<Proforma>
@ -406,6 +407,10 @@ export class SequelizeProformaRepositoryV2
database: this.database,
strictMode: true,
});
const quickSearchWhere = buildProformaQuickSearchWhere({
quickSearch: criteria.quickSearch,
fullTextWhere: criteriaQuery.where as WhereOptions<InferAttributes<ProformaModel>>,
});
/**
* Restricciones propias del repositorio.
@ -431,7 +436,7 @@ export class SequelizeProformaRepositoryV2
baseWhere,
scopeWhere,
...(options.where ? [options.where] : []),
...(criteriaQuery.where ? [criteriaQuery.where] : []),
...(quickSearchWhere ? [quickSearchWhere] : []),
],
};
@ -587,19 +592,31 @@ export class SequelizeProformaRepositoryV2
database: this.database,
strictMode: true,
});
const quickSearchWhere = buildProformaQuickSearchWhere({
quickSearch: criteria.quickSearch,
fullTextWhere: criteriaQuery.where as WhereOptions<InferAttributes<ProformaModel>>,
});
const where: WhereOptions<InferAttributes<ProformaModel>> = {
[Op.and]: [
{ company_id: companyId.toString() },
this.buildScopeWhere(scope),
...(options.where ? [options.where] : []),
...(criteriaQuery.where ? [criteriaQuery.where] : []),
...(quickSearchWhere ? [quickSearchWhere] : []),
],
};
const rows = (await ProformaModel.findAll({
attributes: ["status", [fn("COUNT", col("ProformaModel.id")), "count"]],
group: ["status"],
include: [
{
model: this.database.models.CustomerModel,
as: "current_customer",
required: false,
attributes: [],
},
],
paranoid: scope !== "deleted",
raw: true,
transaction,