productos, equivalencias, cross-sell y cantidades

This commit is contained in:
Lucas Tettamanti
2026-01-18 18:28:28 -03:00
parent 8cc4744c49
commit c7c56ddbfc
32 changed files with 4083 additions and 2073 deletions

View File

@@ -594,7 +594,7 @@ export async function getRecoRulesByProductIds({ tenant_id, product_ids = [] })
export async function getRecoRuleByKey({ tenant_id, rule_key }) {
const sql = `
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
trigger_product_ids, recommended_product_ids, created_at, updated_at
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, created_at, updated_at
from product_reco_rules
where tenant_id=$1 and rule_key=$2
limit 1
@@ -603,6 +603,95 @@ export async function getRecoRuleByKey({ tenant_id, rule_key }) {
return rows[0] || null;
}
/**
* Obtener reglas de qty_per_person por tipo de evento (asado, horno, etc.)
* DEPRECATED: Usar getProductQtyRulesByEvent en su lugar
*/
export async function getQtyPerPersonRules({ tenant_id, event_type }) {
const sql = `
select r.id, r.rule_key, r.rule_type, r.trigger_event, r.priority,
json_agg(json_build_object(
'woo_product_id', i.woo_product_id,
'audience_type', i.audience_type,
'qty_per_person', i.qty_per_person,
'unit', i.unit,
'reason', i.reason,
'display_order', i.display_order
) order by i.display_order) as items
from product_reco_rules r
inner join reco_rule_items i on i.rule_id = r.id
where r.tenant_id = $1
and r.active = true
and r.rule_type = 'qty_per_person'
and (r.trigger_event = $2 or r.trigger_event is null)
group by r.id, r.rule_key, r.rule_type, r.trigger_event, r.priority
order by
case when r.trigger_event = $2 then 0 else 1 end,
r.priority asc
`;
const { rows } = await pool.query(sql, [tenant_id, event_type]);
return rows;
}
/**
* Obtener reglas de cantidad por evento desde la nueva tabla product_qty_rules
*/
export async function getProductQtyRulesByEvent({ tenant_id, event_type }) {
const sql = `
select woo_product_id, event_type, person_type, qty_per_person, unit
from product_qty_rules
where tenant_id = $1 and event_type = $2
order by woo_product_id, person_type
`;
const { rows } = await pool.query(sql, [tenant_id, event_type]);
return rows;
}
/**
* Obtener items de una regla específica con detalles
*/
export async function getRecoRuleItems({ rule_id }) {
const sql = `
select id, rule_id, woo_product_id, audience_type, qty_per_person, unit, reason, display_order
from reco_rule_items
where rule_id = $1
order by display_order asc
`;
const { rows } = await pool.query(sql, [rule_id]);
return rows;
}
/**
* Obtener productos mapeados a un alias con scores
*/
export async function getAliasProductMappings({ tenant_id, alias }) {
const normalizedAlias = String(alias || "").toLowerCase().trim();
if (!normalizedAlias) return [];
const sql = `
select woo_product_id, score
from alias_product_mappings
where tenant_id = $1 and alias = $2
order by score desc
`;
const { rows } = await pool.query(sql, [tenant_id, normalizedAlias]);
return rows;
}
/**
* Obtener todos los mappings de alias para un tenant (para búsqueda)
*/
export async function getAllAliasProductMappings({ tenant_id }) {
const sql = `
select alias, woo_product_id, score
from alias_product_mappings
where tenant_id = $1
order by alias, score desc
`;
const { rows } = await pool.query(sql, [tenant_id]);
return rows;
}
export async function getProductEmbedding({ tenant_id, content_hash }) {
const sql = `
select tenant_id, content_hash, content_text, embedding, model, updated_at

View File

@@ -338,7 +338,13 @@ const runStatus = llmMeta?.error ? "warn" : "ok";
railguard: { simulated: isSimulated, source: meta?.source || null },
woo_customer_error: wooCustomerError,
};
const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state;
// El nuevo FSM usa context.order, extraerlo para safeNextState
const orderForFsm = context?.order || context?.order_basket || {};
const signals = {
confirm_order: plan.intent === "confirm_order",
payment_selected: plan.intent === "select_payment",
};
const nextState = safeNextState(prev_state, orderForFsm, signals).next_state;
plan.next_state = nextState;
const stateRow = await upsertConversationState({
@@ -366,8 +372,17 @@ const nextState = safeNextState(prev_state, context, { requested_checkout: plan.
await updateRunLatency({ tenant_id: tenantId, run_id, latency_ms: end_to_end_ms });
}
// Incluir carrito completo para la UI
const fullBasket = context?.order_basket?.items || [];
// Incluir carrito completo para la UI (nuevo formato order.cart o legacy order_basket)
const orderData = context?.order || {};
const fullBasket = (orderData.cart || []).map(c => ({
product_id: c.woo_id,
woo_product_id: c.woo_id,
quantity: c.qty,
unit: c.unit,
label: c.name,
name: c.name,
price: c.price,
}));
sseSend("run.created", {
run_id,
@@ -377,6 +392,8 @@ const nextState = safeNextState(prev_state, context, { requested_checkout: plan.
status: runStatus,
prev_state,
input: { text },
// Incluir order completo para la UI
order: orderData,
llm_output: { ...plan, _llm: llmMeta, full_basket: { items: fullBasket } },
tools,
invariants,