separated in modules
This commit is contained in:
648
src/modules/2-identity/db/repo.js
Normal file
648
src/modules/2-identity/db/repo.js
Normal file
@@ -0,0 +1,648 @@
|
||||
import { pool } from "./pool.js";
|
||||
|
||||
export async function ensureTenant({ key, name }) {
|
||||
const q = `
|
||||
insert into tenants (key, name)
|
||||
values ($1, $2)
|
||||
on conflict (key) do update set name = excluded.name
|
||||
returning id
|
||||
`;
|
||||
const { rows } = await pool.query(q, [key, name]);
|
||||
return rows[0].id;
|
||||
}
|
||||
|
||||
export async function getConversationState(tenant_id, wa_chat_id) {
|
||||
const q = `
|
||||
select tenant_id, wa_chat_id, state, last_intent, last_order_id, context, state_updated_at
|
||||
from wa_conversation_state
|
||||
where tenant_id=$1 and wa_chat_id=$2
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function upsertConversationState({
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
state,
|
||||
last_intent = null,
|
||||
last_order_id = null,
|
||||
context = {},
|
||||
}) {
|
||||
const q = `
|
||||
insert into wa_conversation_state
|
||||
(tenant_id, wa_chat_id, state, state_updated_at, last_intent, last_order_id, context, updated_at)
|
||||
values
|
||||
($1, $2, $3, now(), $4, $5, $6::jsonb, now())
|
||||
on conflict (tenant_id, wa_chat_id)
|
||||
do update set
|
||||
state = excluded.state,
|
||||
state_updated_at = now(),
|
||||
last_intent = excluded.last_intent,
|
||||
last_order_id = excluded.last_order_id,
|
||||
context = excluded.context,
|
||||
updated_at = now()
|
||||
returning tenant_id, wa_chat_id, state, last_intent, last_order_id, context, state_updated_at, updated_at
|
||||
`;
|
||||
const { rows } = await pool.query(q, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
state,
|
||||
last_intent,
|
||||
last_order_id,
|
||||
JSON.stringify(context ?? {}),
|
||||
]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Crea la conversación si no existe y, si existe, solo “toca” updated_at (no pisa state/context).
|
||||
export async function touchConversationState({ tenant_id, wa_chat_id }) {
|
||||
const q = `
|
||||
insert into wa_conversation_state
|
||||
(tenant_id, wa_chat_id, state, state_updated_at, last_intent, last_order_id, context, updated_at)
|
||||
values
|
||||
($1, $2, 'IDLE', now(), 'other', null, '{}'::jsonb, now())
|
||||
on conflict (tenant_id, wa_chat_id)
|
||||
do update set
|
||||
updated_at = now()
|
||||
returning tenant_id, wa_chat_id, state, last_intent, context, updated_at
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function insertMessage({
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
provider,
|
||||
message_id,
|
||||
direction, // in|out
|
||||
text = null,
|
||||
payload = {},
|
||||
run_id = null,
|
||||
ts = null,
|
||||
}) {
|
||||
const q = `
|
||||
insert into wa_messages
|
||||
(tenant_id, wa_chat_id, provider, message_id, direction, ts, text, payload, run_id)
|
||||
values
|
||||
($1, $2, $3, $4, $5, coalesce($6::timestamptz, now()), $7, $8::jsonb, $9)
|
||||
on conflict (tenant_id, provider, message_id) do nothing
|
||||
returning id
|
||||
`;
|
||||
const { rows } = await pool.query(q, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
provider,
|
||||
message_id,
|
||||
direction,
|
||||
ts,
|
||||
text,
|
||||
JSON.stringify(payload ?? {}),
|
||||
run_id,
|
||||
]);
|
||||
return rows[0]?.id || null;
|
||||
}
|
||||
|
||||
export async function insertRun({
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
message_id,
|
||||
prev_state = null,
|
||||
user_text = null,
|
||||
llm_output = null,
|
||||
tools = [],
|
||||
invariants = {},
|
||||
final_reply = null,
|
||||
order_id = null,
|
||||
payment_link = null,
|
||||
status = "ok",
|
||||
error_code = null,
|
||||
error_detail = null,
|
||||
latency_ms = null,
|
||||
}) {
|
||||
const q = `
|
||||
insert into conversation_runs
|
||||
(tenant_id, wa_chat_id, message_id, ts, prev_state, user_text, llm_output, tools, invariants,
|
||||
final_reply, order_id, payment_link, status, error_code, error_detail, latency_ms)
|
||||
values
|
||||
($1, $2, $3, now(), $4, $5, $6::jsonb, $7::jsonb, $8::jsonb,
|
||||
$9, $10, $11, $12, $13, $14, $15)
|
||||
on conflict (tenant_id, message_id) do nothing
|
||||
returning id
|
||||
`;
|
||||
const { rows } = await pool.query(q, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
message_id,
|
||||
prev_state,
|
||||
user_text,
|
||||
llm_output ? JSON.stringify(llm_output) : null,
|
||||
JSON.stringify(tools ?? []),
|
||||
JSON.stringify(invariants ?? {}),
|
||||
final_reply,
|
||||
order_id,
|
||||
payment_link,
|
||||
status,
|
||||
error_code,
|
||||
error_detail,
|
||||
latency_ms,
|
||||
]);
|
||||
return rows[0]?.id || null;
|
||||
}
|
||||
|
||||
export async function updateRunLatency({ tenant_id, run_id, latency_ms }) {
|
||||
if (!tenant_id || !run_id) return false;
|
||||
const q = `
|
||||
update conversation_runs
|
||||
set latency_ms = $3
|
||||
where tenant_id = $1 and id = $2
|
||||
`;
|
||||
await pool.query(q, [tenant_id, run_id, latency_ms]);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function listConversations({ tenant_id, q = "", status = "", state = "", limit = 50 }) {
|
||||
const params = [tenant_id];
|
||||
let where = `where tenant_id=$1`;
|
||||
|
||||
if (q) {
|
||||
params.push(`%${q}%`);
|
||||
where += ` and wa_chat_id ilike $${params.length}`;
|
||||
}
|
||||
if (status) {
|
||||
// status derivado no implementado en MVP
|
||||
}
|
||||
if (state) {
|
||||
params.push(state);
|
||||
where += ` and state = $${params.length}`;
|
||||
}
|
||||
|
||||
const qsql = `
|
||||
select tenant_id, wa_chat_id,
|
||||
state,
|
||||
coalesce(last_intent,'other') as intent,
|
||||
updated_at as last_activity
|
||||
from wa_conversation_state
|
||||
${where}
|
||||
order by updated_at desc
|
||||
limit ${Math.max(1, Math.min(200, limit))}
|
||||
`;
|
||||
|
||||
const { rows } = await pool.query(qsql, params);
|
||||
|
||||
return rows.map((r) => ({
|
||||
chat_id: r.wa_chat_id,
|
||||
from: r.wa_chat_id.replace(/^sim:/, ""),
|
||||
state: r.state,
|
||||
intent: r.intent,
|
||||
status: "ok",
|
||||
last_activity: r.last_activity,
|
||||
last_run_id: null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listRuns({ tenant_id, wa_chat_id = null, limit = 50 }) {
|
||||
const params = [tenant_id];
|
||||
let where = `where tenant_id=$1`;
|
||||
if (wa_chat_id) {
|
||||
params.push(wa_chat_id);
|
||||
where += ` and wa_chat_id=$${params.length}`;
|
||||
}
|
||||
const q = `
|
||||
select id as run_id, ts, wa_chat_id as chat_id,
|
||||
status, prev_state, user_text,
|
||||
llm_output, tools, invariants,
|
||||
final_reply, order_id, payment_link, latency_ms
|
||||
from conversation_runs
|
||||
${where}
|
||||
order by ts desc
|
||||
limit ${Math.max(1, Math.min(200, limit))}
|
||||
`;
|
||||
const { rows } = await pool.query(q, params);
|
||||
|
||||
return rows.map((r) => ({
|
||||
run_id: r.run_id,
|
||||
ts: r.ts,
|
||||
chat_id: r.chat_id,
|
||||
from: r.chat_id.replace(/^sim:/, ""),
|
||||
status: r.status,
|
||||
prev_state: r.prev_state,
|
||||
input: { text: r.user_text },
|
||||
llm_output: r.llm_output,
|
||||
tools: r.tools,
|
||||
invariants: r.invariants,
|
||||
final_reply: r.final_reply,
|
||||
order_id: r.order_id,
|
||||
payment_link: r.payment_link,
|
||||
latency_ms: r.latency_ms,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getRunById({ tenant_id, run_id }) {
|
||||
const q = `
|
||||
select id as run_id, ts, wa_chat_id as chat_id,
|
||||
status, prev_state, user_text,
|
||||
llm_output, tools, invariants,
|
||||
final_reply, order_id, payment_link, latency_ms
|
||||
from conversation_runs
|
||||
where tenant_id=$1 and id=$2
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, run_id]);
|
||||
const r = rows[0];
|
||||
if (!r) return null;
|
||||
return {
|
||||
run_id: r.run_id,
|
||||
ts: r.ts,
|
||||
chat_id: r.chat_id,
|
||||
from: r.chat_id.replace(/^sim:/, ""),
|
||||
status: r.status,
|
||||
prev_state: r.prev_state,
|
||||
input: { text: r.user_text },
|
||||
llm_output: r.llm_output,
|
||||
tools: r.tools,
|
||||
invariants: r.invariants,
|
||||
final_reply: r.final_reply,
|
||||
order_id: r.order_id,
|
||||
payment_link: r.payment_link,
|
||||
latency_ms: r.latency_ms,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRecentMessagesForLLM({
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
limit = 20,
|
||||
maxCharsPerMessage = 800,
|
||||
}) {
|
||||
const lim = Math.max(1, Math.min(50, parseInt(limit, 10) || 20));
|
||||
const q = `
|
||||
select direction, ts, text
|
||||
from wa_messages
|
||||
where tenant_id=$1
|
||||
and wa_chat_id=$2
|
||||
and text is not null
|
||||
and length(trim(text)) > 0
|
||||
order by ts desc
|
||||
limit $3
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id, lim]);
|
||||
|
||||
return rows.reverse().map((r) => ({
|
||||
role: r.direction === "in" ? "user" : "assistant",
|
||||
content: String(r.text).trim().slice(0, maxCharsPerMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listMessages({ tenant_id, wa_chat_id, limit = 200 }) {
|
||||
const lim = Math.max(1, Math.min(500, parseInt(limit, 10) || 200));
|
||||
const q = `
|
||||
select provider, message_id, direction, ts, text, payload, run_id
|
||||
from wa_messages
|
||||
where tenant_id=$1 and wa_chat_id=$2
|
||||
order by ts asc
|
||||
limit $3
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id, lim]);
|
||||
return rows.map((r) => ({
|
||||
provider: r.provider,
|
||||
message_id: r.message_id,
|
||||
direction: r.direction,
|
||||
ts: r.ts,
|
||||
text: r.text,
|
||||
payload: r.payload,
|
||||
run_id: r.run_id,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function deleteConversationData({ tenant_id, wa_chat_id }) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const r1 = await client.query(`delete from wa_messages where tenant_id=$1 and wa_chat_id=$2`, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
]);
|
||||
const r2 = await client.query(`delete from conversation_runs where tenant_id=$1 and wa_chat_id=$2`, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
]);
|
||||
const r3 = await client.query(`delete from wa_conversation_state where tenant_id=$1 and wa_chat_id=$2`, [
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
]);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { ok: true, deleted: { messages: r1.rowCount, runs: r2.rowCount, state: r3.rowCount } };
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function listUsers({ tenant_id, q = "", limit = 200 }) {
|
||||
const lim = Math.max(1, Math.min(500, parseInt(limit, 10) || 200));
|
||||
const qstr = String(q || "").trim();
|
||||
|
||||
// Lista de “usuarios” = conversaciones existentes (wa_conversation_state), con join al mapping Woo si existe.
|
||||
// Esto permite ver usuarios aunque nunca se haya creado el customer en Woo.
|
||||
const sql = `
|
||||
select *
|
||||
from (
|
||||
select s.wa_chat_id,
|
||||
'woo' as provider,
|
||||
m.external_customer_id,
|
||||
lastmsg.ts as last_ts,
|
||||
nullif(coalesce(lastmsg.payload #>> '{raw,meta,pushName}', lastmsg.payload #>> '{raw,meta,pushname}', ''), '') as push_name
|
||||
from wa_conversation_state s
|
||||
left join wa_identity_map m
|
||||
on m.tenant_id = s.tenant_id
|
||||
and m.wa_chat_id = s.wa_chat_id
|
||||
and m.provider = 'woo'
|
||||
left join lateral (
|
||||
select ts, payload
|
||||
from wa_messages
|
||||
where tenant_id = s.tenant_id
|
||||
and wa_chat_id = s.wa_chat_id
|
||||
and direction = 'in'
|
||||
order by ts desc
|
||||
limit 1
|
||||
) lastmsg on true
|
||||
where s.tenant_id = $1
|
||||
) t
|
||||
where ($2 = '' or t.wa_chat_id ilike $3 or coalesce(t.push_name,'') ilike $3)
|
||||
order by coalesce(t.last_ts, now()) desc
|
||||
limit $4
|
||||
`;
|
||||
|
||||
const like = qstr ? `%${qstr}%` : "";
|
||||
const { rows } = await pool.query(sql, [tenant_id, qstr, like, lim]);
|
||||
return rows.map((r) => ({
|
||||
chat_id: r.wa_chat_id,
|
||||
provider: r.provider,
|
||||
external_customer_id: r.external_customer_id,
|
||||
push_name: r.push_name || null,
|
||||
last_ts: r.last_ts || null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getLastInboundMessage({ tenant_id, wa_chat_id }) {
|
||||
const q = `
|
||||
select provider, message_id, ts, text, payload
|
||||
from wa_messages
|
||||
where tenant_id=$1 and wa_chat_id=$2 and direction='in'
|
||||
order by ts desc
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function cleanupLastRunForRetry({ tenant_id, wa_chat_id }) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const { rows } = await client.query(
|
||||
`
|
||||
select id
|
||||
from conversation_runs
|
||||
where tenant_id=$1 and wa_chat_id=$2
|
||||
order by ts desc
|
||||
limit 1
|
||||
`,
|
||||
[tenant_id, wa_chat_id]
|
||||
);
|
||||
const run_id = rows[0]?.id || null;
|
||||
if (!run_id) {
|
||||
await client.query("COMMIT");
|
||||
return { ok: true, run_id: null, deleted_out_messages: 0, deleted_runs: 0 };
|
||||
}
|
||||
|
||||
const r1 = await client.query(
|
||||
`delete from wa_messages where tenant_id=$1 and wa_chat_id=$2 and run_id=$3 and direction='out'`,
|
||||
[tenant_id, wa_chat_id, run_id]
|
||||
);
|
||||
const r2 = await client.query(`delete from conversation_runs where tenant_id=$1 and id=$2`, [tenant_id, run_id]);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { ok: true, run_id, deleted_out_messages: r1.rowCount || 0, deleted_runs: r2.rowCount || 0 };
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getIdentityMapByChat({ tenant_id, wa_chat_id, provider = "woo" }) {
|
||||
const q = `
|
||||
select tenant_id, wa_chat_id, provider, external_customer_id, created_at, updated_at
|
||||
from wa_identity_map
|
||||
where tenant_id=$1 and wa_chat_id=$2 and provider=$3
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id, provider]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function deleteIdentityMapByChat({ tenant_id, wa_chat_id, provider = "woo" }) {
|
||||
const q = `delete from wa_identity_map where tenant_id=$1 and wa_chat_id=$2 and provider=$3`;
|
||||
const { rowCount } = await pool.query(q, [tenant_id, wa_chat_id, provider]);
|
||||
return rowCount || 0;
|
||||
}
|
||||
|
||||
export async function getTenantByKey(key) {
|
||||
const { rows } = await pool.query(`select id, key, name from tenants where key=$1`, [key]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function getTenantIdByChannel({ channel_type, channel_key }) {
|
||||
const q = `
|
||||
select tenant_id
|
||||
from tenant_channels
|
||||
where channel_type=$1 and channel_key=$2
|
||||
`;
|
||||
const { rows } = await pool.query(q, [channel_type, channel_key]);
|
||||
return rows[0]?.tenant_id || null;
|
||||
}
|
||||
|
||||
export async function getExternalCustomerIdByChat({ tenant_id, wa_chat_id, provider = "woo" }) {
|
||||
const q = `
|
||||
select external_customer_id
|
||||
from wa_identity_map
|
||||
where tenant_id=$1 and wa_chat_id=$2 and provider=$3
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id, provider]);
|
||||
return rows[0]?.external_customer_id || null;
|
||||
}
|
||||
|
||||
export async function upsertExternalCustomerMap({
|
||||
tenant_id,
|
||||
wa_chat_id,
|
||||
external_customer_id,
|
||||
provider = "woo",
|
||||
}) {
|
||||
const q = `
|
||||
insert into wa_identity_map (tenant_id, wa_chat_id, provider, external_customer_id, created_at, updated_at)
|
||||
values ($1, $2, $3, $4, now(), now())
|
||||
on conflict (tenant_id, wa_chat_id)
|
||||
do update set external_customer_id = excluded.external_customer_id, updated_at = now()
|
||||
returning external_customer_id
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, wa_chat_id, provider, external_customer_id]);
|
||||
return rows[0]?.external_customer_id || null;
|
||||
}
|
||||
|
||||
export async function getTenantEcommerceConfig({ tenant_id, provider = "woo" }) {
|
||||
const q = `
|
||||
select id, tenant_id, provider, base_url, credential_ref, api_version, timeout_ms,
|
||||
enc_consumer_key, enc_consumer_secret, encryption_salt, enabled
|
||||
from tenant_ecommerce_config
|
||||
where tenant_id = $1 and provider = $2 and enabled = true
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, provider]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function getDecryptedTenantEcommerceConfig({
|
||||
tenant_id,
|
||||
provider = "woo",
|
||||
encryption_key,
|
||||
}) {
|
||||
if (!encryption_key) {
|
||||
throw new Error("encryption_key is required to decrypt ecommerce credentials");
|
||||
}
|
||||
const q = `
|
||||
select id, tenant_id, provider, base_url, credential_ref, api_version, timeout_ms, enabled,
|
||||
pgp_sym_decrypt(enc_consumer_key, $3)::text as consumer_key,
|
||||
pgp_sym_decrypt(enc_consumer_secret, $3)::text as consumer_secret
|
||||
from tenant_ecommerce_config
|
||||
where tenant_id = $1 and provider = $2 and enabled = true
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(q, [tenant_id, provider, encryption_key]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function searchProductAliases({ tenant_id, q = "", limit = 20 }) {
|
||||
const lim = Math.max(1, Math.min(200, parseInt(limit, 10) || 20));
|
||||
const query = String(q || "").trim();
|
||||
if (!query) return [];
|
||||
const normalized = query.toLowerCase();
|
||||
const like = `%${query}%`;
|
||||
const nlike = `%${normalized}%`;
|
||||
const sql = `
|
||||
select tenant_id, alias, normalized_alias, woo_product_id, category_hint, boost, metadata, updated_at
|
||||
from product_aliases
|
||||
where tenant_id=$1
|
||||
and (alias ilike $2 or normalized_alias ilike $3)
|
||||
order by boost desc, updated_at desc
|
||||
limit $4
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenant_id, like, nlike, lim]);
|
||||
return rows.map((r) => ({
|
||||
tenant_id: r.tenant_id,
|
||||
alias: r.alias,
|
||||
normalized_alias: r.normalized_alias,
|
||||
woo_product_id: r.woo_product_id,
|
||||
category_hint: r.category_hint,
|
||||
boost: r.boost,
|
||||
metadata: r.metadata,
|
||||
updated_at: r.updated_at,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getProductEmbedding({ tenant_id, content_hash }) {
|
||||
const sql = `
|
||||
select tenant_id, content_hash, content_text, embedding, model, updated_at
|
||||
from product_embeddings_cache
|
||||
where tenant_id=$1 and content_hash=$2
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenant_id, content_hash]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function upsertProductEmbedding({
|
||||
tenant_id,
|
||||
content_hash,
|
||||
content_text,
|
||||
embedding,
|
||||
model,
|
||||
}) {
|
||||
const sql = `
|
||||
insert into product_embeddings_cache
|
||||
(tenant_id, content_hash, content_text, embedding, model, updated_at)
|
||||
values
|
||||
($1, $2, $3, $4::jsonb, $5, now())
|
||||
on conflict (tenant_id, content_hash)
|
||||
do update set
|
||||
content_text = excluded.content_text,
|
||||
embedding = excluded.embedding,
|
||||
model = excluded.model,
|
||||
updated_at = now()
|
||||
returning tenant_id, content_hash, content_text, embedding, model, updated_at
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [
|
||||
tenant_id,
|
||||
content_hash,
|
||||
content_text,
|
||||
JSON.stringify(embedding ?? []),
|
||||
model,
|
||||
]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function upsertMpPayment({
|
||||
tenant_id,
|
||||
woo_order_id = null,
|
||||
preference_id = null,
|
||||
payment_id = null,
|
||||
status = null,
|
||||
paid_at = null,
|
||||
raw = {},
|
||||
}) {
|
||||
if (!payment_id) throw new Error("payment_id_required");
|
||||
const sql = `
|
||||
insert into mp_payments
|
||||
(tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, created_at, updated_at)
|
||||
values
|
||||
($1, $2, $3, $4, $5, $6::timestamptz, $7::jsonb, now(), now())
|
||||
on conflict (tenant_id, payment_id)
|
||||
do update set
|
||||
woo_order_id = excluded.woo_order_id,
|
||||
preference_id = excluded.preference_id,
|
||||
status = excluded.status,
|
||||
paid_at = excluded.paid_at,
|
||||
raw = excluded.raw,
|
||||
updated_at = now()
|
||||
returning tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, updated_at
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [
|
||||
tenant_id,
|
||||
woo_order_id,
|
||||
preference_id,
|
||||
payment_id,
|
||||
status,
|
||||
paid_at,
|
||||
JSON.stringify(raw ?? {}),
|
||||
]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function getMpPaymentById({ tenant_id, payment_id }) {
|
||||
const sql = `
|
||||
select tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, updated_at
|
||||
from mp_payments
|
||||
where tenant_id=$1 and payment_id=$2
|
||||
limit 1
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [tenant_id, payment_id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user