productos, equivalencias, cross-sell y cantidades
This commit is contained in:
251
src/modules/3-turn-engine/orderModel.js
Normal file
251
src/modules/3-turn-engine/orderModel.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Modelo unificado de orden para el contexto de conversación.
|
||||
*
|
||||
* Reemplaza: order_basket, pending_items, pending_clarification, pending_item,
|
||||
* checkout_step, payment_method, shipping_method, delivery_address
|
||||
*/
|
||||
|
||||
// Status de items pendientes
|
||||
export const PendingStatus = Object.freeze({
|
||||
NEEDS_TYPE: "NEEDS_TYPE", // Necesita seleccionar producto de opciones
|
||||
NEEDS_QUANTITY: "NEEDS_QUANTITY", // Necesita especificar cantidad
|
||||
READY: "READY", // Listo para mover a cart
|
||||
});
|
||||
|
||||
/**
|
||||
* Crea una orden vacía
|
||||
*/
|
||||
export function createEmptyOrder() {
|
||||
return {
|
||||
cart: [], // Items confirmados: [{ woo_id, qty, unit, name, price }]
|
||||
pending: [], // Items por clarificar
|
||||
payment_type: null, // "link" | "cash" | null
|
||||
is_delivery: null, // true | false | null
|
||||
shipping_address: null,
|
||||
woo_order_id: null,
|
||||
is_paid: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un item de carrito confirmado
|
||||
*/
|
||||
export function createCartItem({ woo_id, qty, unit, name = null, price = null }) {
|
||||
return {
|
||||
woo_id,
|
||||
qty: Number(qty) || 1,
|
||||
unit: unit || "unit", // "kg" | "g" | "unit"
|
||||
name,
|
||||
price,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un item pendiente de clarificación
|
||||
*/
|
||||
export function createPendingItem({
|
||||
id,
|
||||
query,
|
||||
candidates = [],
|
||||
selected_woo_id = null,
|
||||
selected_name = null,
|
||||
selected_price = null,
|
||||
selected_unit = null,
|
||||
qty = null,
|
||||
unit = null,
|
||||
status = PendingStatus.NEEDS_TYPE
|
||||
}) {
|
||||
return {
|
||||
id: id || `pending_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
query,
|
||||
candidates, // [{ woo_id, name, price, display_unit }]
|
||||
selected_woo_id, // Producto elegido (null si NEEDS_TYPE)
|
||||
selected_name,
|
||||
selected_price,
|
||||
selected_unit, // Unidad del producto seleccionado
|
||||
qty, // Cantidad (null si NEEDS_QUANTITY)
|
||||
unit, // Unidad elegida por usuario
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mueve items READY de pending a cart
|
||||
*/
|
||||
export function moveReadyToCart(order) {
|
||||
if (!order) return createEmptyOrder();
|
||||
|
||||
const newCart = [...(order.cart || [])];
|
||||
const newPending = [];
|
||||
|
||||
for (const item of (order.pending || [])) {
|
||||
if (item.status === PendingStatus.READY && item.selected_woo_id && item.qty) {
|
||||
// Buscar si ya existe en cart
|
||||
const existingIdx = newCart.findIndex(c => c.woo_id === item.selected_woo_id);
|
||||
const cartItem = createCartItem({
|
||||
woo_id: item.selected_woo_id,
|
||||
qty: item.qty,
|
||||
unit: item.unit || item.selected_unit || "unit",
|
||||
name: item.selected_name,
|
||||
price: item.selected_price,
|
||||
});
|
||||
|
||||
if (existingIdx >= 0) {
|
||||
// Actualizar cantidad existente
|
||||
newCart[existingIdx] = {
|
||||
...newCart[existingIdx],
|
||||
qty: cartItem.qty,
|
||||
unit: cartItem.unit,
|
||||
};
|
||||
} else {
|
||||
newCart.push(cartItem);
|
||||
}
|
||||
} else {
|
||||
newPending.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...order,
|
||||
cart: newCart,
|
||||
pending: newPending,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el primer item pendiente que necesita clarificación
|
||||
*/
|
||||
export function getNextPendingItem(order) {
|
||||
if (!Array.isArray(order?.pending)) return null;
|
||||
return order.pending.find(i =>
|
||||
i.status === PendingStatus.NEEDS_TYPE ||
|
||||
i.status === PendingStatus.NEEDS_QUANTITY
|
||||
) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza un item pendiente por ID
|
||||
*/
|
||||
export function updatePendingItem(order, itemId, updates) {
|
||||
if (!order?.pending) return order;
|
||||
|
||||
const newPending = order.pending.map(item => {
|
||||
if (item.id === itemId) {
|
||||
return { ...item, ...updates };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
return { ...order, pending: newPending };
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega un nuevo item pendiente
|
||||
*/
|
||||
export function addPendingItem(order, pendingItem) {
|
||||
const current = order || createEmptyOrder();
|
||||
return {
|
||||
...current,
|
||||
pending: [...(current.pending || []), pendingItem],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte orden vieja (order_basket, pending_items, etc.) a nuevo formato
|
||||
*/
|
||||
export function migrateOldContext(ctx) {
|
||||
if (!ctx) return createEmptyOrder();
|
||||
|
||||
// Si ya tiene el nuevo formato
|
||||
if (ctx.order && (Array.isArray(ctx.order.cart) || Array.isArray(ctx.order.pending))) {
|
||||
return ctx.order;
|
||||
}
|
||||
|
||||
const order = createEmptyOrder();
|
||||
|
||||
// Migrar order_basket
|
||||
if (ctx.order_basket?.items) {
|
||||
order.cart = ctx.order_basket.items.map(item => createCartItem({
|
||||
woo_id: item.product_id || item.woo_id || item.woo_product_id,
|
||||
qty: item.quantity || item.qty || 1,
|
||||
unit: item.unit || "unit",
|
||||
name: item.label || item.name,
|
||||
price: item.price,
|
||||
}));
|
||||
}
|
||||
|
||||
// Migrar pending_items
|
||||
if (Array.isArray(ctx.pending_items)) {
|
||||
order.pending = ctx.pending_items.map(item => createPendingItem({
|
||||
id: item.id,
|
||||
query: item.query,
|
||||
candidates: (item.candidates || []).map(c => ({
|
||||
woo_id: c.woo_product_id || c.woo_id,
|
||||
name: c.name,
|
||||
price: c.price,
|
||||
display_unit: c.display_unit,
|
||||
})),
|
||||
selected_woo_id: item.resolved_product?.woo_product_id || item.resolved_product?.woo_id,
|
||||
selected_name: item.resolved_product?.name,
|
||||
selected_price: item.resolved_product?.price,
|
||||
selected_unit: item.resolved_product?.display_unit,
|
||||
qty: item.quantity,
|
||||
unit: item.unit,
|
||||
status: item.status === "needs_type" ? PendingStatus.NEEDS_TYPE :
|
||||
item.status === "needs_quantity" ? PendingStatus.NEEDS_QUANTITY :
|
||||
item.status === "ready" ? PendingStatus.READY :
|
||||
item.status?.toUpperCase() || PendingStatus.NEEDS_TYPE,
|
||||
}));
|
||||
}
|
||||
|
||||
// Migrar checkout info
|
||||
order.payment_type = ctx.payment_method || null;
|
||||
order.is_delivery = ctx.shipping_method === "delivery" ? true :
|
||||
ctx.shipping_method === "pickup" ? false : null;
|
||||
order.shipping_address = ctx.delivery_address?.text || ctx.address?.text || ctx.address_text || null;
|
||||
order.woo_order_id = ctx.woo_order_id || ctx.last_order_id || null;
|
||||
order.is_paid = ctx.mp?.payment_status === "approved" ||
|
||||
ctx.payment?.status === "paid" ||
|
||||
ctx.payment_status === "approved" || false;
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea el carrito para mostrar al usuario
|
||||
*/
|
||||
export function formatCartForDisplay(order, locale = "es-AR") {
|
||||
if (!order?.cart?.length) {
|
||||
return "Tu carrito está vacío.";
|
||||
}
|
||||
|
||||
const lines = order.cart.map(item => {
|
||||
const qtyStr = item.unit === "kg" ? `${item.qty}kg` :
|
||||
item.unit === "g" ? `${item.qty}g` :
|
||||
`${item.qty}`;
|
||||
return `- ${qtyStr} de ${item.name || `Producto #${item.woo_id}`}`;
|
||||
});
|
||||
|
||||
return "Tenés anotado:\n" + lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea opciones de un pending item para mostrar al usuario
|
||||
*/
|
||||
export function formatOptionsForDisplay(pendingItem, pageSize = 9) {
|
||||
if (!pendingItem?.candidates?.length) {
|
||||
return { question: `No encontré "${pendingItem?.query}". ¿Podrías ser más específico?`, options: [] };
|
||||
}
|
||||
|
||||
const options = pendingItem.candidates.slice(0, pageSize);
|
||||
const hasMore = pendingItem.candidates.length > pageSize;
|
||||
|
||||
const lines = options.map((c, i) => `${i + 1}) ${c.name}`);
|
||||
if (hasMore) {
|
||||
lines.push(`${pageSize + 1}) Mostrame más...`);
|
||||
}
|
||||
|
||||
const question = `Para "${pendingItem.query}", ¿cuál de estos querés?\n` + lines.join("\n") + "\n\nRespondé con el número.";
|
||||
|
||||
return { question, options };
|
||||
}
|
||||
Reference in New Issue
Block a user