productos, equivalencias, cross-sell y cantidades
This commit is contained in:
@@ -17,13 +17,13 @@ export const makeListAliases = (tenantIdOrFn) => async (req, res) => {
|
||||
export const makeCreateAlias = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const { alias, woo_product_id, boost, category_hint, metadata } = req.body || {};
|
||||
const { alias, woo_product_id, boost, category_hint, metadata, product_mappings } = req.body || {};
|
||||
|
||||
if (!alias || !woo_product_id) {
|
||||
return res.status(400).json({ ok: false, error: "alias_and_woo_product_id_required" });
|
||||
}
|
||||
|
||||
const result = await handleCreateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
const result = await handleCreateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata, product_mappings });
|
||||
res.json({ ok: true, item: result });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -38,13 +38,13 @@ export const makeUpdateAlias = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const alias = req.params.alias;
|
||||
const { woo_product_id, boost, category_hint, metadata } = req.body || {};
|
||||
const { woo_product_id, boost, category_hint, metadata, product_mappings } = req.body || {};
|
||||
|
||||
if (!woo_product_id) {
|
||||
return res.status(400).json({ ok: false, error: "woo_product_id_required" });
|
||||
}
|
||||
|
||||
const result = await handleUpdateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
const result = await handleUpdateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata, product_mappings });
|
||||
if (!result) {
|
||||
return res.status(404).json({ ok: false, error: "alias_not_found" });
|
||||
}
|
||||
|
||||
51
src/modules/0-ui/controllers/quantities.js
Normal file
51
src/modules/0-ui/controllers/quantities.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
handleListProductQtyRules,
|
||||
handleGetProductQtyRules,
|
||||
handleSaveProductQtyRules
|
||||
} from "../handlers/quantities.js";
|
||||
|
||||
export const makeListProductQtyRules = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const result = await handleListProductQtyRules({ tenantId });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false, error: "internal_error" });
|
||||
}
|
||||
};
|
||||
|
||||
export const makeGetProductQtyRules = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const wooProductId = parseInt(req.params.wooProductId, 10);
|
||||
|
||||
if (!wooProductId) {
|
||||
return res.status(400).json({ ok: false, error: "woo_product_id_required" });
|
||||
}
|
||||
|
||||
const result = await handleGetProductQtyRules({ tenantId, wooProductId });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false, error: "internal_error" });
|
||||
}
|
||||
};
|
||||
|
||||
export const makeSaveProductQtyRules = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const wooProductId = parseInt(req.params.wooProductId, 10);
|
||||
const { rules } = req.body || {};
|
||||
|
||||
if (!wooProductId) {
|
||||
return res.status(400).json({ ok: false, error: "woo_product_id_required" });
|
||||
}
|
||||
|
||||
const result = await handleSaveProductQtyRules({ tenantId, wooProductId, rules: rules || [] });
|
||||
res.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false, error: "internal_error" });
|
||||
}
|
||||
};
|
||||
@@ -37,14 +37,18 @@ export const makeGetRecommendation = (tenantIdOrFn) => async (req, res) => {
|
||||
export const makeCreateRecommendation = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const { rule_key, trigger, queries, boosts, ask_slots, active, priority } = req.body || {};
|
||||
const {
|
||||
rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
} = req.body || {};
|
||||
|
||||
if (!rule_key) {
|
||||
return res.status(400).json({ ok: false, error: "rule_key_required" });
|
||||
}
|
||||
|
||||
const result = await handleCreateRecommendation({
|
||||
tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority
|
||||
tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
});
|
||||
res.json({ ok: true, item: result });
|
||||
} catch (err) {
|
||||
@@ -60,10 +64,14 @@ export const makeUpdateRecommendation = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const id = req.params.id;
|
||||
const { trigger, queries, boosts, ask_slots, active, priority } = req.body || {};
|
||||
const {
|
||||
trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
} = req.body || {};
|
||||
|
||||
const result = await handleUpdateRecommendation({
|
||||
tenantId, id, trigger, queries, boosts, ask_slots, active, priority
|
||||
tenantId, id, trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
});
|
||||
if (!result) {
|
||||
return res.status(404).json({ ok: false, error: "recommendation_not_found" });
|
||||
|
||||
@@ -16,17 +16,19 @@ export async function listProducts({ tenantId, q = "", limit = 2000, offset = 0
|
||||
select
|
||||
woo_id as woo_product_id,
|
||||
name,
|
||||
slug as sku,
|
||||
coalesce(raw->>'SKU', raw->>'sku', slug) as sku,
|
||||
slug,
|
||||
price_current as price,
|
||||
stock_status,
|
||||
stock_qty,
|
||||
categories,
|
||||
attributes_normalized,
|
||||
updated_at as refreshed_at,
|
||||
raw as payload,
|
||||
raw->>'_sell_unit_override' as sell_unit
|
||||
coalesce(raw->>'_sell_unit_override', 'kg') as sell_unit
|
||||
from woo_products_snapshot
|
||||
where tenant_id = $1
|
||||
and (name ilike $2 or coalesce(slug,'') ilike $2)
|
||||
and (name ilike $2 or coalesce(slug,'') ilike $2 or coalesce(raw->>'SKU', raw->>'sku', '') ilike $2)
|
||||
order by name asc
|
||||
limit $3 offset $4
|
||||
`;
|
||||
@@ -36,14 +38,16 @@ export async function listProducts({ tenantId, q = "", limit = 2000, offset = 0
|
||||
select
|
||||
woo_id as woo_product_id,
|
||||
name,
|
||||
slug as sku,
|
||||
coalesce(raw->>'SKU', raw->>'sku', slug) as sku,
|
||||
slug,
|
||||
price_current as price,
|
||||
stock_status,
|
||||
stock_qty,
|
||||
categories,
|
||||
attributes_normalized,
|
||||
updated_at as refreshed_at,
|
||||
raw as payload,
|
||||
raw->>'_sell_unit_override' as sell_unit
|
||||
coalesce(raw->>'_sell_unit_override', 'kg') as sell_unit
|
||||
from woo_products_snapshot
|
||||
where tenant_id = $1
|
||||
order by name asc
|
||||
@@ -61,14 +65,16 @@ export async function getProductByWooId({ tenantId, wooProductId }) {
|
||||
select
|
||||
woo_id as woo_product_id,
|
||||
name,
|
||||
slug as sku,
|
||||
coalesce(raw->>'sku', slug) as sku,
|
||||
slug,
|
||||
price_current as price,
|
||||
stock_status,
|
||||
stock_qty,
|
||||
categories,
|
||||
attributes_normalized,
|
||||
updated_at as refreshed_at,
|
||||
raw as payload,
|
||||
raw->>'_sell_unit_override' as sell_unit
|
||||
coalesce(raw->>'_sell_unit_override', 'kg') as sell_unit
|
||||
from woo_products_snapshot
|
||||
where tenant_id = $1 and woo_id = $2
|
||||
limit 1
|
||||
@@ -101,29 +107,37 @@ export async function bulkUpdateProductSellUnit({ tenantId, wooProductIds, sell_
|
||||
}
|
||||
|
||||
export async function updateProduct({ tenantId, wooProductId, sell_unit, categories }) {
|
||||
// Build the JSONB update dynamically
|
||||
// Build the update - combine all raw updates into one
|
||||
let updates = [];
|
||||
let params = [tenantId, wooProductId];
|
||||
let paramIdx = 3;
|
||||
|
||||
// Build the raw column update by chaining jsonb_set calls
|
||||
let rawExpr = "coalesce(raw, '{}'::jsonb)";
|
||||
|
||||
if (sell_unit) {
|
||||
updates.push(`raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_sell_unit_override}', $${paramIdx}::jsonb)`);
|
||||
rawExpr = `jsonb_set(${rawExpr}, '{_sell_unit_override}', $${paramIdx}::jsonb)`;
|
||||
params.push(JSON.stringify(sell_unit));
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (categories) {
|
||||
// Also update the categories column if it exists
|
||||
// Update categories column
|
||||
updates.push(`categories = $${paramIdx}::jsonb`);
|
||||
params.push(JSON.stringify(categories.map(name => ({ name }))));
|
||||
paramIdx++;
|
||||
|
||||
// Also store in raw for persistence
|
||||
updates.push(`raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_categories_override}', $${paramIdx}::jsonb)`);
|
||||
// Chain the categories override into raw
|
||||
rawExpr = `jsonb_set(${rawExpr}, '{_categories_override}', $${paramIdx}::jsonb)`;
|
||||
params.push(JSON.stringify(categories));
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
// Only add raw update if we modified it
|
||||
if (sell_unit || categories) {
|
||||
updates.push(`raw = ${rawExpr}`);
|
||||
}
|
||||
|
||||
if (!updates.length) return null;
|
||||
|
||||
const sql = `
|
||||
@@ -185,6 +199,31 @@ export async function listAliases({ tenantId, q = "", woo_product_id = null, lim
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(sql, params);
|
||||
|
||||
// Cargar mappings para cada alias
|
||||
if (rows.length > 0) {
|
||||
const mappingsSql = `
|
||||
select alias, woo_product_id, score
|
||||
from alias_product_mappings
|
||||
where tenant_id = $1 and alias = any($2::text[])
|
||||
order by alias, score desc
|
||||
`;
|
||||
const aliases = rows.map(r => r.alias);
|
||||
const { rows: mappings } = await pool.query(mappingsSql, [tenantId, aliases]);
|
||||
|
||||
// Agrupar mappings por alias
|
||||
const mappingsByAlias = {};
|
||||
for (const m of mappings) {
|
||||
if (!mappingsByAlias[m.alias]) mappingsByAlias[m.alias] = [];
|
||||
mappingsByAlias[m.alias].push({ woo_product_id: m.woo_product_id, score: m.score });
|
||||
}
|
||||
|
||||
// Agregar mappings a cada alias
|
||||
for (const row of rows) {
|
||||
row.product_mappings = mappingsByAlias[row.alias] || [];
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -236,9 +275,62 @@ export async function updateAlias({ tenantId, alias, woo_product_id, boost = 0,
|
||||
export async function deleteAlias({ tenantId, alias }) {
|
||||
const sql = `delete from product_aliases where tenant_id = $1 and alias = $2 returning alias`;
|
||||
const { rows } = await pool.query(sql, [tenantId, alias.toLowerCase().trim()]);
|
||||
// También eliminar mappings asociados
|
||||
await pool.query(`delete from alias_product_mappings where tenant_id = $1 and alias = $2`, [tenantId, alias.toLowerCase().trim()]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Alias Product Mappings (multi-producto)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listAliasMappings({ tenantId, alias }) {
|
||||
const sql = `
|
||||
select alias, woo_product_id, score, created_at
|
||||
from alias_product_mappings
|
||||
where tenant_id = $1 and alias = $2
|
||||
order by score desc
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId, alias.toLowerCase().trim()]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function upsertAliasMapping({ tenantId, alias, woo_product_id, score = 1.0 }) {
|
||||
const sql = `
|
||||
insert into alias_product_mappings (tenant_id, alias, woo_product_id, score)
|
||||
values ($1, $2, $3, $4)
|
||||
on conflict (tenant_id, alias, woo_product_id)
|
||||
do update set score = $4
|
||||
returning alias, woo_product_id, score, created_at
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId, alias.toLowerCase().trim(), woo_product_id, score]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function deleteAliasMapping({ tenantId, alias, woo_product_id }) {
|
||||
const sql = `delete from alias_product_mappings where tenant_id = $1 and alias = $2 and woo_product_id = $3 returning alias`;
|
||||
const { rows } = await pool.query(sql, [tenantId, alias.toLowerCase().trim(), woo_product_id]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function setAliasMappings({ tenantId, alias, mappings }) {
|
||||
const normalizedAlias = alias.toLowerCase().trim();
|
||||
|
||||
// Eliminar mappings existentes
|
||||
await pool.query(`delete from alias_product_mappings where tenant_id = $1 and alias = $2`, [tenantId, normalizedAlias]);
|
||||
|
||||
// Insertar nuevos mappings
|
||||
if (mappings && mappings.length > 0) {
|
||||
const insertSql = `
|
||||
insert into alias_product_mappings (tenant_id, alias, woo_product_id, score)
|
||||
values ($1, $2, $3, $4)
|
||||
`;
|
||||
for (const mapping of mappings) {
|
||||
await pool.query(insertSql, [tenantId, normalizedAlias, mapping.woo_product_id, mapping.score ?? 1.0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Recommendations
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -252,7 +344,7 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
||||
const like = `%${query}%`;
|
||||
sql = `
|
||||
select 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 ilike $2
|
||||
order by priority desc, rule_key asc
|
||||
@@ -262,7 +354,7 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
||||
} else {
|
||||
sql = `
|
||||
select 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
|
||||
order by priority desc, rule_key asc
|
||||
@@ -278,13 +370,24 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
||||
export async function getRecommendationById({ tenantId, id }) {
|
||||
const sql = `
|
||||
select 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 id = $2
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId, id]);
|
||||
return rows[0] || null;
|
||||
if (!rows[0]) return null;
|
||||
|
||||
// Cargar items asociados
|
||||
const itemsSql = `
|
||||
select 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: items } = await pool.query(itemsSql, [id]);
|
||||
|
||||
return { ...rows[0], items };
|
||||
}
|
||||
|
||||
export async function insertRecommendation({
|
||||
@@ -298,11 +401,14 @@ export async function insertRecommendation({
|
||||
priority = 100,
|
||||
trigger_product_ids = [],
|
||||
recommended_product_ids = [],
|
||||
rule_type = "crosssell",
|
||||
trigger_event = null,
|
||||
items = [],
|
||||
}) {
|
||||
const sql = `
|
||||
insert into product_reco_rules (tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||
insert into product_reco_rules (tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, rule_type, trigger_event)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, rule_type, trigger_event, created_at, updated_at
|
||||
`;
|
||||
|
||||
const { rows } = await pool.query(sql, [
|
||||
@@ -316,9 +422,18 @@ export async function insertRecommendation({
|
||||
priority || 100,
|
||||
trigger_product_ids || [],
|
||||
recommended_product_ids || [],
|
||||
rule_type || "crosssell",
|
||||
trigger_event || null,
|
||||
]);
|
||||
|
||||
return rows[0];
|
||||
const rule = rows[0];
|
||||
|
||||
// Insertar items si hay
|
||||
if (items && items.length > 0) {
|
||||
await upsertRecoRuleItems({ ruleId: rule.id, items });
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
export async function updateRecommendation({
|
||||
@@ -332,6 +447,9 @@ export async function updateRecommendation({
|
||||
priority,
|
||||
trigger_product_ids,
|
||||
recommended_product_ids,
|
||||
rule_type,
|
||||
trigger_event,
|
||||
items,
|
||||
}) {
|
||||
const sql = `
|
||||
update product_reco_rules
|
||||
@@ -344,9 +462,11 @@ export async function updateRecommendation({
|
||||
priority = $8,
|
||||
trigger_product_ids = $9,
|
||||
recommended_product_ids = $10,
|
||||
rule_type = $11,
|
||||
trigger_event = $12,
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, rule_type, trigger_event, created_at, updated_at
|
||||
`;
|
||||
|
||||
const { rows } = await pool.query(sql, [
|
||||
@@ -360,13 +480,147 @@ export async function updateRecommendation({
|
||||
priority || 100,
|
||||
trigger_product_ids || [],
|
||||
recommended_product_ids || [],
|
||||
rule_type || "crosssell",
|
||||
trigger_event || null,
|
||||
]);
|
||||
|
||||
// Actualizar items si se proporcionan
|
||||
if (items !== undefined) {
|
||||
await upsertRecoRuleItems({ ruleId: id, items: items || [] });
|
||||
}
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function upsertRecoRuleItems({ ruleId, items }) {
|
||||
// Eliminar items existentes
|
||||
await pool.query(`delete from reco_rule_items where rule_id = $1`, [ruleId]);
|
||||
|
||||
// Insertar nuevos items
|
||||
if (items && items.length > 0) {
|
||||
const insertSql = `
|
||||
insert into reco_rule_items (rule_id, woo_product_id, audience_type, qty_per_person, unit, reason, display_order)
|
||||
values ($1, $2, $3, $4, $5, $6, $7)
|
||||
`;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
await pool.query(insertSql, [
|
||||
ruleId,
|
||||
item.woo_product_id,
|
||||
item.audience_type ?? "adult",
|
||||
item.qty_per_person ?? null,
|
||||
item.unit ?? null,
|
||||
item.reason ?? null,
|
||||
item.display_order ?? i,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRecommendation({ tenantId, id }) {
|
||||
// Los items se eliminan en cascada
|
||||
const sql = `delete from product_reco_rules where tenant_id = $1 and id = $2 returning id`;
|
||||
const { rows } = await pool.query(sql, [tenantId, id]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product Quantity Rules (cantidades por producto/evento/persona)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Obtener todas las reglas de cantidad agrupadas por producto
|
||||
*/
|
||||
export async function listProductQtyRules({ tenantId }) {
|
||||
const sql = `
|
||||
select id, woo_product_id, event_type, person_type, qty_per_person, unit, updated_at
|
||||
from product_qty_rules
|
||||
where tenant_id = $1
|
||||
order by woo_product_id, event_type, person_type
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener reglas de un producto específico
|
||||
*/
|
||||
export async function getProductQtyRules({ tenantId, wooProductId }) {
|
||||
const sql = `
|
||||
select id, woo_product_id, event_type, person_type, qty_per_person, unit, updated_at
|
||||
from product_qty_rules
|
||||
where tenant_id = $1 and woo_product_id = $2
|
||||
order by event_type, person_type
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId, wooProductId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert una regla de cantidad (crear o actualizar)
|
||||
*/
|
||||
export async function upsertProductQtyRule({ tenantId, wooProductId, eventType, personType, qtyPerPerson, unit }) {
|
||||
const sql = `
|
||||
insert into product_qty_rules (tenant_id, woo_product_id, event_type, person_type, qty_per_person, unit)
|
||||
values ($1, $2, $3, $4, $5, $6)
|
||||
on conflict (tenant_id, woo_product_id, event_type, person_type)
|
||||
do update set
|
||||
qty_per_person = $5,
|
||||
unit = $6,
|
||||
updated_at = now()
|
||||
returning id, woo_product_id, event_type, person_type, qty_per_person, unit, updated_at
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId, wooProductId, eventType, personType, qtyPerPerson, unit || 'kg']);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar una regla de cantidad específica
|
||||
*/
|
||||
export async function deleteProductQtyRule({ tenantId, id }) {
|
||||
const sql = `delete from product_qty_rules where tenant_id = $1 and id = $2 returning id`;
|
||||
const { rows } = await pool.query(sql, [tenantId, id]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar todas las reglas de un producto (reemplaza las existentes)
|
||||
*/
|
||||
export async function saveProductQtyRules({ tenantId, wooProductId, rules }) {
|
||||
// Eliminar reglas existentes del producto
|
||||
await pool.query(`delete from product_qty_rules where tenant_id = $1 and woo_product_id = $2`, [tenantId, wooProductId]);
|
||||
|
||||
// Insertar nuevas reglas
|
||||
if (rules && rules.length > 0) {
|
||||
const insertSql = `
|
||||
insert into product_qty_rules (tenant_id, woo_product_id, event_type, person_type, qty_per_person, unit)
|
||||
values ($1, $2, $3, $4, $5, $6)
|
||||
`;
|
||||
for (const rule of rules) {
|
||||
if (rule.qty_per_person != null && rule.qty_per_person > 0) {
|
||||
await pool.query(insertSql, [
|
||||
tenantId,
|
||||
wooProductId,
|
||||
rule.event_type,
|
||||
rule.person_type,
|
||||
rule.qty_per_person,
|
||||
rule.unit || 'kg'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contar reglas por producto (para mostrar badges)
|
||||
*/
|
||||
export async function countQtyRulesByProduct({ tenantId }) {
|
||||
const sql = `
|
||||
select woo_product_id, count(*) as rule_count
|
||||
from product_qty_rules
|
||||
where tenant_id = $1
|
||||
group by woo_product_id
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenantId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,64 @@
|
||||
import { listAliases, insertAlias, updateAlias, deleteAlias } from "../db/repo.js";
|
||||
import {
|
||||
listAliases,
|
||||
insertAlias,
|
||||
updateAlias,
|
||||
deleteAlias,
|
||||
listAliasMappings,
|
||||
setAliasMappings,
|
||||
} from "../db/repo.js";
|
||||
|
||||
export async function handleListAliases({ tenantId, q = "", woo_product_id = null, limit = 200 }) {
|
||||
const items = await listAliases({ tenantId, q, woo_product_id, limit });
|
||||
return { items };
|
||||
}
|
||||
|
||||
export async function handleCreateAlias({ tenantId, alias, woo_product_id, boost = 0, category_hint = null, metadata = {} }) {
|
||||
return insertAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
export async function handleCreateAlias({
|
||||
tenantId,
|
||||
alias,
|
||||
woo_product_id,
|
||||
boost = 0,
|
||||
category_hint = null,
|
||||
metadata = {},
|
||||
product_mappings = [],
|
||||
}) {
|
||||
const result = await insertAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
|
||||
// Si hay mappings, guardarlos
|
||||
if (product_mappings && product_mappings.length > 0) {
|
||||
await setAliasMappings({ tenantId, alias, mappings: product_mappings });
|
||||
} else if (woo_product_id) {
|
||||
// Si solo hay un producto, crear mapping por defecto
|
||||
await setAliasMappings({ tenantId, alias, mappings: [{ woo_product_id, score: boost || 1.0 }] });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function handleUpdateAlias({ tenantId, alias, woo_product_id, boost = 0, category_hint = null, metadata = {} }) {
|
||||
return updateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
export async function handleUpdateAlias({
|
||||
tenantId,
|
||||
alias,
|
||||
woo_product_id,
|
||||
boost = 0,
|
||||
category_hint = null,
|
||||
metadata = {},
|
||||
product_mappings,
|
||||
}) {
|
||||
const result = await updateAlias({ tenantId, alias, woo_product_id, boost, category_hint, metadata });
|
||||
|
||||
// Si hay mappings, actualizarlos
|
||||
if (product_mappings !== undefined) {
|
||||
await setAliasMappings({ tenantId, alias, mappings: product_mappings || [] });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function handleDeleteAlias({ tenantId, alias }) {
|
||||
const deleted = await deleteAlias({ tenantId, alias });
|
||||
return { deleted };
|
||||
}
|
||||
|
||||
export async function handleGetAliasMappings({ tenantId, alias }) {
|
||||
const mappings = await listAliasMappings({ tenantId, alias });
|
||||
return { mappings };
|
||||
}
|
||||
|
||||
23
src/modules/0-ui/handlers/quantities.js
Normal file
23
src/modules/0-ui/handlers/quantities.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
listProductQtyRules,
|
||||
getProductQtyRules,
|
||||
saveProductQtyRules,
|
||||
countQtyRulesByProduct,
|
||||
} from "../db/repo.js";
|
||||
|
||||
export async function handleListProductQtyRules({ tenantId }) {
|
||||
const rules = await listProductQtyRules({ tenantId });
|
||||
const counts = await countQtyRulesByProduct({ tenantId });
|
||||
return { rules, counts };
|
||||
}
|
||||
|
||||
export async function handleGetProductQtyRules({ tenantId, wooProductId }) {
|
||||
const rules = await getProductQtyRules({ tenantId, wooProductId });
|
||||
return { rules };
|
||||
}
|
||||
|
||||
export async function handleSaveProductQtyRules({ tenantId, wooProductId, rules }) {
|
||||
await saveProductQtyRules({ tenantId, wooProductId, rules });
|
||||
const updated = await getProductQtyRules({ tenantId, wooProductId });
|
||||
return { rules: updated };
|
||||
}
|
||||
@@ -26,8 +26,14 @@ export async function handleCreateRecommendation({
|
||||
priority = 100,
|
||||
trigger_product_ids = [],
|
||||
recommended_product_ids = [],
|
||||
rule_type = "crosssell",
|
||||
trigger_event = null,
|
||||
items = [],
|
||||
}) {
|
||||
return insertRecommendation({ tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids });
|
||||
return insertRecommendation({
|
||||
tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleUpdateRecommendation({
|
||||
@@ -41,8 +47,14 @@ export async function handleUpdateRecommendation({
|
||||
priority,
|
||||
trigger_product_ids,
|
||||
recommended_product_ids,
|
||||
rule_type,
|
||||
trigger_event,
|
||||
items,
|
||||
}) {
|
||||
return updateRecommendation({ tenantId, id, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids });
|
||||
return updateRecommendation({
|
||||
tenantId, id, trigger, queries, boosts, ask_slots, active, priority,
|
||||
trigger_product_ids, recommended_product_ids, rule_type, trigger_event, items
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleDeleteRecommendation({ tenantId, id }) {
|
||||
|
||||
Reference in New Issue
Block a user