modules/0-ui

This commit is contained in:
Lucas Tettamanti
2026-01-15 22:53:37 -03:00
parent ea62385e3d
commit 98e3d78e3d
17 changed files with 17 additions and 23 deletions

View File

@@ -0,0 +1,47 @@
import { handleDeleteConversation, handleDeleteUser, handleListUsers, handleRetryLast } from "../handlers/admin.js";
export const makeDeleteConversation = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const result = await handleDeleteConversation({ tenantId, chat_id: req.params.chat_id });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};
export const makeListUsers = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const result = await handleListUsers({ tenantId, q: req.query.q || "", limit: req.query.limit || "200" });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};
export const makeDeleteUser = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const deleteWoo = String(req.query.deleteWoo || "0") === "1" || String(req.query.deleteWoo || "").toLowerCase() === "true";
const result = await handleDeleteUser({ tenantId, chat_id: req.params.chat_id, deleteWoo });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};
export const makeRetryLast = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const result = await handleRetryLast({ tenantId, chat_id: req.params.chat_id });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,16 @@
import { handleGetConversationState } from "../handlers/conversationState.js";
export const makeGetConversationState = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const { status, payload } = await handleGetConversationState({
tenantId,
chat_id: req.query.chat_id || null,
});
res.status(status).json(payload);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,13 @@
import { handleListConversations } from "../handlers/conversations.js";
export const makeGetConversations = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const items = await handleListConversations({ tenantId, query: req.query });
res.json({ items });
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,17 @@
import { handleListMessages } from "../handlers/messages.js";
export const makeListMessages = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const items = await handleListMessages({
tenantId,
chat_id: req.query.chat_id || null,
limit: req.query.limit || "200",
});
res.json({ items });
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,16 @@
import { handleSearchProducts } from "../handlers/products.js";
export const makeSearchProducts = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const q = req.query.q || "";
const limit = req.query.limit || "10";
const forceWoo = req.query.forceWoo || "0";
const result = await handleSearchProducts({ tenantId, q, limit, forceWoo });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,29 @@
import { handleListRuns, handleGetRun } from "../handlers/runs.js";
export const makeListRuns = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const items = await handleListRuns({
tenantId,
chat_id: req.query.chat_id || null,
limit: req.query.limit || "50",
});
res.json({ items });
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};
export const makeGetRunById = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const run = await handleGetRun({ tenantId, run_id: req.params.run_id });
if (!run) return res.status(404).json({ ok: false, error: "not_found" });
res.json(run);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error" });
}
};

View File

@@ -0,0 +1,90 @@
import crypto from "crypto";
import {
cleanupLastRunForRetry,
deleteConversationData,
deleteIdentityMapByChat,
getIdentityMapByChat,
getLastInboundMessage,
listUsers,
} from "../../2-identity/db/repo.js";
import { deleteWooCustomer } from "../../2-identity/services/woo.js";
import { processMessage } from "../../2-identity/services/pipeline.js";
export async function handleDeleteConversation({ tenantId, chat_id }) {
if (!chat_id) return { ok: false, error: "chat_id_required" };
const result = await deleteConversationData({ tenant_id: tenantId, wa_chat_id: String(chat_id) });
return { ok: true, ...result };
}
export async function handleListUsers({ tenantId, q = "", limit = "200" }) {
const items = await listUsers({ tenant_id: tenantId, q: String(q || ""), limit: parseInt(limit, 10) || 200 });
return { ok: true, items };
}
export async function handleDeleteUser({ tenantId, chat_id, deleteWoo = false }) {
const wa_chat_id = String(chat_id || "");
if (!wa_chat_id) return { ok: false, error: "chat_id_required" };
const mapping = await getIdentityMapByChat({ tenant_id: tenantId, wa_chat_id, provider: "woo" });
const external_customer_id = mapping?.external_customer_id || null;
// borrar conversaciones (mensajes + runs + state)
const convo = await deleteConversationData({ tenant_id: tenantId, wa_chat_id });
// borrar mapping
const deletedMap = await deleteIdentityMapByChat({ tenant_id: tenantId, wa_chat_id, provider: "woo" });
// borrar customer en woo (best-effort)
let woo = null;
if (deleteWoo && external_customer_id) {
try {
woo = await deleteWooCustomer({ tenantId, id: external_customer_id, force: true });
} catch (e) {
woo = { ok: false, error: String(e?.message || e), status: e?.status || e?.cause?.status || null };
}
}
return {
ok: true,
chat_id: wa_chat_id,
external_customer_id,
deleted: {
conversations: convo?.deleted || null,
identity_map: deletedMap,
woo,
},
};
}
export async function handleRetryLast({ tenantId, chat_id }) {
const wa_chat_id = String(chat_id || "");
if (!wa_chat_id) return { ok: false, error: "chat_id_required" };
const lastIn = await getLastInboundMessage({ tenant_id: tenantId, wa_chat_id });
if (!lastIn) return { ok: false, error: "no_inbound_message" };
// 1) borrar la última respuesta/run
const cleanup = await cleanupLastRunForRetry({ tenant_id: tenantId, wa_chat_id });
// 2) reinyectar el último mensaje del usuario como un nuevo message_id
const raw = lastIn.payload?.raw || {};
const text = String(lastIn.text || raw.text || "").trim();
if (!text) return { ok: false, error: "last_inbound_text_empty", cleanup };
const from = String(raw.from || wa_chat_id.replace(/@.+$/, ""));
const pushName = raw?.meta?.pushName || raw?.meta?.pushname || null;
const pm = await processMessage({
tenantId,
chat_id: wa_chat_id,
from,
text,
provider: lastIn.provider || "evolution",
message_id: crypto.randomUUID(),
displayName: pushName,
meta: { ...(raw.meta || {}), source: "retry_last" },
});
return { ok: true, cleanup, run_id: pm?.run_id || null };
}

View File

@@ -0,0 +1,21 @@
import { getConversationState } from "../../2-identity/db/repo.js";
export async function handleGetConversationState({ tenantId, chat_id }) {
if (!chat_id) {
return { status: 400, payload: { ok: false, error: "chat_id required" } };
}
const row = await getConversationState(tenantId, chat_id);
if (!row) return { status: 404, payload: { ok: false, error: "not_found" } };
return {
status: 200,
payload: {
ok: true,
state: row.state,
last_intent: row.last_intent,
last_order_id: row.last_order_id,
context: row.context,
state_updated_at: row.state_updated_at,
},
};
}

View File

@@ -0,0 +1,13 @@
import { listConversations } from "../../2-identity/db/repo.js";
export async function handleListConversations({ tenantId, query }) {
const { q = "", status = "", state = "", limit = "50" } = query || {};
return listConversations({
tenant_id: tenantId,
q: String(q || ""),
status: String(status || ""),
state: String(state || ""),
limit: parseInt(limit, 10) || 50,
});
}

View File

@@ -0,0 +1,11 @@
import { listMessages } from "../../2-identity/db/repo.js";
export async function handleListMessages({ tenantId, chat_id, limit = "200" }) {
if (!chat_id) return [];
return listMessages({
tenant_id: tenantId,
wa_chat_id: String(chat_id),
limit: parseInt(limit, 10) || 200,
});
}

View File

@@ -0,0 +1,11 @@
import { searchSnapshotItems } from "../../shared/wooSnapshot.js";
export async function handleSearchProducts({ tenantId, q = "", limit = "10", forceWoo = "0" }) {
const { items, source } = await searchSnapshotItems({
tenantId,
q,
limit: parseInt(limit, 10) || 10,
});
return { items, source };
}

View File

@@ -0,0 +1,14 @@
import { listRuns, getRunById } from "../../2-identity/db/repo.js";
export async function handleListRuns({ tenantId, chat_id = null, limit = "50" }) {
return listRuns({
tenant_id: tenantId,
wa_chat_id: chat_id ? String(chat_id) : null,
limit: parseInt(limit, 10) || 50,
});
}
export async function handleGetRun({ tenantId, run_id }) {
return getRunById({ tenant_id: tenantId, run_id });
}