20260204
This commit is contained in:
@@ -5,7 +5,6 @@ import { fileURLToPath } from "url";
|
||||
|
||||
import { createSimulatorRouter } from "./modules/1-intake/routes/simulator.js";
|
||||
import { createEvolutionRouter } from "./modules/1-intake/routes/evolution.js";
|
||||
import { createMercadoPagoRouter } from "./modules/6-mercadopago/routes/mercadoPago.js";
|
||||
import { createWooWebhooksRouter } from "./modules/2-identity/routes/wooWebhooks.js";
|
||||
|
||||
export function createApp({ tenantId }) {
|
||||
@@ -23,7 +22,6 @@ export function createApp({ tenantId }) {
|
||||
// --- Integraciones / UI ---
|
||||
app.use(createSimulatorRouter({ tenantId }));
|
||||
app.use(createEvolutionRouter());
|
||||
app.use("/payments/meli", createMercadoPagoRouter());
|
||||
app.use(createWooWebhooksRouter());
|
||||
|
||||
// Home (UI)
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
handleListOrders,
|
||||
handleGetProductsWithStock,
|
||||
handleCreateTestOrder,
|
||||
handleCreatePaymentLink,
|
||||
handleSimulateMpWebhook,
|
||||
} from "../handlers/testing.js";
|
||||
import { handleGetOrderStats } from "../handlers/stats.js";
|
||||
|
||||
@@ -59,47 +57,3 @@ export const makeCreateTestOrder = (tenantIdOrFn) => async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const makeCreatePaymentLink = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const { woo_order_id, amount } = req.body || {};
|
||||
|
||||
if (!woo_order_id) {
|
||||
return res.status(400).json({ ok: false, error: "woo_order_id_required" });
|
||||
}
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
return res.status(400).json({ ok: false, error: "amount_required" });
|
||||
}
|
||||
|
||||
const result = await handleCreatePaymentLink({
|
||||
tenantId,
|
||||
wooOrderId: woo_order_id,
|
||||
amount: Number(amount)
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error("[testing] createPaymentLink error:", err);
|
||||
res.status(500).json({ ok: false, error: err.message || "internal_error" });
|
||||
}
|
||||
};
|
||||
|
||||
export const makeSimulateMpWebhook = (tenantIdOrFn) => async (req, res) => {
|
||||
try {
|
||||
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||
const { woo_order_id, amount } = req.body || {};
|
||||
|
||||
if (!woo_order_id) {
|
||||
return res.status(400).json({ ok: false, error: "woo_order_id_required" });
|
||||
}
|
||||
|
||||
const result = await handleSimulateMpWebhook({
|
||||
tenantId,
|
||||
wooOrderId: woo_order_id,
|
||||
amount: Number(amount) || 0
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error("[testing] simulateMpWebhook error:", err);
|
||||
res.status(500).json({ ok: false, error: err.message || "internal_error" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function getSettings({ tenantId }) {
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
schedule,
|
||||
delivery_zones,
|
||||
created_at, updated_at
|
||||
FROM tenant_settings
|
||||
WHERE tenant_id = $1
|
||||
@@ -48,6 +49,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_hours_start,
|
||||
pickup_hours_end,
|
||||
schedule,
|
||||
delivery_zones,
|
||||
} = settings;
|
||||
|
||||
const sql = `
|
||||
@@ -55,9 +57,9 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
tenant_id, store_name, bot_name, store_address, store_phone,
|
||||
delivery_enabled, delivery_days, delivery_hours_start, delivery_hours_end, delivery_min_order,
|
||||
pickup_enabled, pickup_days, pickup_hours_start, pickup_hours_end,
|
||||
schedule
|
||||
schedule, delivery_zones
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
store_name = COALESCE(EXCLUDED.store_name, tenant_settings.store_name),
|
||||
bot_name = COALESCE(EXCLUDED.bot_name, tenant_settings.bot_name),
|
||||
@@ -73,6 +75,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_hours_start = COALESCE(EXCLUDED.pickup_hours_start, tenant_settings.pickup_hours_start),
|
||||
pickup_hours_end = COALESCE(EXCLUDED.pickup_hours_end, tenant_settings.pickup_hours_end),
|
||||
schedule = COALESCE(EXCLUDED.schedule, tenant_settings.schedule),
|
||||
delivery_zones = COALESCE(EXCLUDED.delivery_zones, tenant_settings.delivery_zones),
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
id, tenant_id,
|
||||
@@ -85,6 +88,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
schedule,
|
||||
delivery_zones,
|
||||
created_at, updated_at
|
||||
`;
|
||||
|
||||
@@ -104,6 +108,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_hours_start || null,
|
||||
pickup_hours_end || null,
|
||||
schedule ? JSON.stringify(schedule) : null,
|
||||
delivery_zones ? JSON.stringify(delivery_zones) : null,
|
||||
];
|
||||
|
||||
const { rows } = await pool.query(sql, params);
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function handleGetSettings({ tenantId }) {
|
||||
pickup_hours_start: "08:00",
|
||||
pickup_hours_end: "20:00",
|
||||
schedule: createDefaultSchedule(),
|
||||
delivery_zones: {},
|
||||
is_default: true,
|
||||
};
|
||||
}
|
||||
@@ -60,6 +61,7 @@ export async function handleGetSettings({ tenantId }) {
|
||||
pickup_hours_start: settings.pickup_hours_start?.slice(0, 5) || "08:00",
|
||||
pickup_hours_end: settings.pickup_hours_end?.slice(0, 5) || "20:00",
|
||||
schedule,
|
||||
delivery_zones: settings.delivery_zones || {},
|
||||
is_default: false,
|
||||
};
|
||||
}
|
||||
@@ -103,7 +105,8 @@ function buildScheduleFromLegacy(settings) {
|
||||
function validateSchedule(schedule) {
|
||||
if (!schedule || typeof schedule !== "object") return;
|
||||
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||
// Acepta HH:MM o HH:MM:SS (la BD puede devolver con segundos)
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
|
||||
|
||||
for (const type of ["delivery", "pickup"]) {
|
||||
const typeSchedule = schedule[type];
|
||||
@@ -240,6 +243,7 @@ export async function handleSaveSettings({ tenantId, settings }) {
|
||||
delivery_hours_end: result.delivery_hours_end?.slice(0, 5),
|
||||
pickup_hours_start: result.pickup_hours_start?.slice(0, 5),
|
||||
pickup_hours_end: result.pickup_hours_end?.slice(0, 5),
|
||||
delivery_zones: result.delivery_zones || {},
|
||||
},
|
||||
message: "Configuración guardada correctamente",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createOrder, syncOrdersIncremental } from "../../4-woo-orders/wooOrders.js";
|
||||
import { createPreference, reconcilePayment } from "../../6-mercadopago/mercadoPago.js";
|
||||
import { listProducts } from "../db/repo.js";
|
||||
import * as ordersRepo from "../../4-woo-orders/ordersRepo.js";
|
||||
|
||||
@@ -73,74 +72,3 @@ export async function handleCreateTestOrder({ tenantId, basket, address, wa_chat
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un link de pago de MercadoPago
|
||||
*/
|
||||
export async function handleCreatePaymentLink({ tenantId, wooOrderId, amount }) {
|
||||
if (!wooOrderId) {
|
||||
throw new Error("missing_woo_order_id");
|
||||
}
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
throw new Error("invalid_amount");
|
||||
}
|
||||
|
||||
const pref = await createPreference({
|
||||
tenantId,
|
||||
wooOrderId,
|
||||
amount: Number(amount),
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
preference_id: pref?.preference_id || null,
|
||||
init_point: pref?.init_point || null,
|
||||
sandbox_init_point: pref?.sandbox_init_point || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simula un webhook de MercadoPago con pago exitoso
|
||||
* No pasa por el endpoint real (requiere firma HMAC)
|
||||
* Crea un payment mock y llama a reconcilePayment directamente
|
||||
*/
|
||||
export async function handleSimulateMpWebhook({ tenantId, wooOrderId, amount }) {
|
||||
if (!wooOrderId) {
|
||||
throw new Error("missing_woo_order_id");
|
||||
}
|
||||
|
||||
// Crear payment mock con status approved
|
||||
const mockPaymentId = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const mockPayment = {
|
||||
id: mockPaymentId,
|
||||
status: "approved",
|
||||
status_detail: "accredited",
|
||||
external_reference: `${tenantId}|${wooOrderId}`,
|
||||
transaction_amount: Number(amount) || 0,
|
||||
currency_id: "ARS",
|
||||
date_approved: new Date().toISOString(),
|
||||
date_created: new Date().toISOString(),
|
||||
payment_method_id: "test",
|
||||
payment_type_id: "credit_card",
|
||||
payer: {
|
||||
email: "test@test.com",
|
||||
},
|
||||
order: {
|
||||
id: `pref-test-${wooOrderId}`,
|
||||
},
|
||||
};
|
||||
|
||||
// Reconciliar el pago (actualiza mp_payments y cambia status de orden a processing)
|
||||
const result = await reconcilePayment({
|
||||
tenantId,
|
||||
payment: mockPayment,
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
payment_id: mockPaymentId,
|
||||
woo_order_id: result?.woo_order_id || wooOrderId,
|
||||
status: "approved",
|
||||
order_status: "processing",
|
||||
reconciled: result?.payment || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { makeListPrompts, makeGetPrompt, makeSavePrompt, makeRollbackPrompt, mak
|
||||
import { makeListPendingTakeovers, makeListAllTakeovers, makeGetTakeover, makeRespondToTakeover, makeCancelTakeover, makeCheckPendingTakeover } from "../../0-ui/controllers/takeovers.js";
|
||||
import { makeGetSettings, makeSaveSettings } from "../../0-ui/controllers/settings.js";
|
||||
import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "../../0-ui/controllers/admin.js";
|
||||
import { makeListOrders, makeGetOrderStats, makeGetProductsWithStock, makeCreateTestOrder, makeCreatePaymentLink, makeSimulateMpWebhook } from "../../0-ui/controllers/testing.js";
|
||||
import { makeListOrders, makeGetOrderStats, makeGetProductsWithStock, makeCreateTestOrder } from "../../0-ui/controllers/testing.js";
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
@@ -114,8 +114,6 @@ export function createSimulatorRouter({ tenantId }) {
|
||||
// --- Testing routes ---
|
||||
router.get("/test/products-with-stock", makeGetProductsWithStock(getTenantId));
|
||||
router.post("/test/order", makeCreateTestOrder(getTenantId));
|
||||
router.post("/test/payment-link", makeCreatePaymentLink(getTenantId));
|
||||
router.post("/test/simulate-webhook", makeSimulateMpWebhook(getTenantId));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -733,51 +733,4 @@ export async function upsertProductEmbedding({
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { debug as dbg } from "../../shared/debug.js";
|
||||
import { runTurnV3 } from "../../3-turn-engine/turnEngineV3.js";
|
||||
import { safeNextState } from "../../3-turn-engine/fsm.js";
|
||||
import { createOrder, updateOrder } from "../../4-woo-orders/wooOrders.js";
|
||||
import { createPreference } from "../../6-mercadopago/mercadoPago.js";
|
||||
import { handleCreateTakeover } from "../../0-ui/handlers/takeovers.js";
|
||||
import { sendTextMessage, isEvolutionEnabled } from "../../1-intake/services/evolutionSender.js";
|
||||
|
||||
@@ -313,25 +312,6 @@ const runStatus = llmMeta?.error ? "warn" : "ok";
|
||||
pending_query: act.payload?.pending_query,
|
||||
});
|
||||
}
|
||||
} else if (act.type === "send_payment_link") {
|
||||
const total = Number(actionPatch?.order_total || reducedContext?.order_total || 0) || null;
|
||||
if (!total || total <= 0) {
|
||||
throw new Error("order_total_missing");
|
||||
}
|
||||
const pref = await createPreference({
|
||||
tenantId,
|
||||
wooOrderId: actionPatch.woo_order_id || reducedContext?.woo_order_id || prev?.last_order_id,
|
||||
amount: total || 0,
|
||||
});
|
||||
actionPatch.payment_link = pref?.init_point || null;
|
||||
actionPatch.mp = {
|
||||
preference_id: pref?.preference_id || null,
|
||||
init_point: pref?.init_point || null,
|
||||
};
|
||||
newTools.push({ type: "send_payment_link", ok: true, preference_id: pref?.preference_id || null });
|
||||
if (pref?.init_point) {
|
||||
plan.reply = `Listo, acá tenés el link de pago: ${pref.init_point}\nAvisame cuando esté listo.`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
newTools.push({ type: act.type, ok: false, error: String(e?.message || e) });
|
||||
|
||||
@@ -162,6 +162,12 @@ function shouldSkipRouter(text, state, quickDomain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// En estado SHIPPING, si quickDomain ya detectó "shipping" (dirección), confiar en eso
|
||||
// Esto evita que el router LLM clasifique direcciones como productos
|
||||
if (state === "SHIPPING" && quickDomain === "shipping") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,6 @@ export async function handlePaymentState({ tenantId, text, nlu, order, audit })
|
||||
currentOrder = { ...currentOrder, payment_type: paymentMethod };
|
||||
actions.push({ type: "create_order", payload: { payment: paymentMethod } });
|
||||
|
||||
if (paymentMethod === "link") {
|
||||
actions.push({ type: "send_payment_link", payload: {} });
|
||||
}
|
||||
|
||||
const { next_state } = safeNextState(ConversationState.PAYMENT, currentOrder, { payment_selected: true });
|
||||
|
||||
const paymentLabel = paymentMethod === "cash" ? "efectivo" : "link de pago";
|
||||
@@ -43,7 +39,7 @@ export async function handlePaymentState({ tenantId, text, nlu, order, audit })
|
||||
: "Retiro en sucursal.";
|
||||
|
||||
const paymentInfo = paymentMethod === "link"
|
||||
? "Te paso el link de pago en un momento."
|
||||
? "Te contactamos para coordinar el pago."
|
||||
: "Pagás en " + (currentOrder.is_delivery ? "la entrega" : "sucursal") + ".";
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { fetchPayment, reconcilePayment, verifyWebhookSignature } from "../mercadoPago.js";
|
||||
|
||||
export function makeMercadoPagoWebhook() {
|
||||
return async function handleMercadoPagoWebhook(req, res) {
|
||||
try {
|
||||
const signature = verifyWebhookSignature({ headers: req.headers, query: req.query || {} });
|
||||
if (!signature.ok) {
|
||||
return res.status(401).json({ ok: false, error: "invalid_signature", reason: signature.reason });
|
||||
}
|
||||
|
||||
const paymentId =
|
||||
req?.query?.["data.id"] ||
|
||||
req?.query?.data?.id ||
|
||||
req?.body?.data?.id ||
|
||||
null;
|
||||
|
||||
if (!paymentId) {
|
||||
return res.status(400).json({ ok: false, error: "missing_payment_id" });
|
||||
}
|
||||
|
||||
const payment = await fetchPayment({ paymentId });
|
||||
const reconciled = await reconcilePayment({ payment });
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
payment_id: payment?.id || null,
|
||||
status: payment?.status || null,
|
||||
woo_order_id: reconciled?.woo_order_id || null,
|
||||
});
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e?.message || e) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMercadoPagoReturn() {
|
||||
return function handleMercadoPagoReturn(req, res) {
|
||||
const status = req.query?.status || "unknown";
|
||||
res.status(200).send(`OK - ${status}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
import { upsertMpPayment } from "../2-identity/db/repo.js";
|
||||
import { updateOrderStatus } from "../4-woo-orders/wooOrders.js";
|
||||
|
||||
function getAccessToken() {
|
||||
return process.env.MP_ACCESS_TOKEN || null;
|
||||
}
|
||||
|
||||
function getWebhookSecret() {
|
||||
return process.env.MP_WEBHOOK_SECRET || null;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(base) {
|
||||
if (!base) return null;
|
||||
return base.endsWith("/") ? base : `${base}/`;
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
return normalizeBaseUrl(process.env.MP_BASE_URL || process.env.MP_WEBHOOK_BASE_URL || null);
|
||||
}
|
||||
|
||||
async function fetchMp({ url, method = "GET", body = null }) {
|
||||
const token = getAccessToken();
|
||||
if (!token) throw new Error("MP_ACCESS_TOKEN is not set");
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
let parsed;
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = new Error(`MP HTTP ${res.status}`);
|
||||
err.status = res.status;
|
||||
err.body = parsed;
|
||||
throw err;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function createPreference({
|
||||
tenantId,
|
||||
wooOrderId,
|
||||
amount,
|
||||
payer = null,
|
||||
items = null,
|
||||
baseUrl = null,
|
||||
}) {
|
||||
const root = normalizeBaseUrl(baseUrl || getBaseUrl());
|
||||
if (!root) throw new Error("MP_BASE_URL is not set");
|
||||
const notificationUrl = `${root}webhook/mercadopago`;
|
||||
const backUrls = {
|
||||
success: `${root}return?status=success`,
|
||||
failure: `${root}return?status=failure`,
|
||||
pending: `${root}return?status=pending`,
|
||||
};
|
||||
const statementDescriptor = process.env.MP_STATEMENT_DESCRIPTOR || "Whatsapp Store";
|
||||
const externalReference = `${tenantId}|${wooOrderId}`;
|
||||
const unitPrice = Number(amount);
|
||||
if (!Number.isFinite(unitPrice)) throw new Error("invalid_amount");
|
||||
|
||||
const payload = {
|
||||
auto_return: "approved",
|
||||
back_urls: backUrls,
|
||||
statement_descriptor: statementDescriptor,
|
||||
binary_mode: false,
|
||||
external_reference: externalReference,
|
||||
items: Array.isArray(items) && items.length
|
||||
? items
|
||||
: [
|
||||
{
|
||||
id: String(wooOrderId || "order"),
|
||||
title: "Productos x whatsapp",
|
||||
quantity: 1,
|
||||
currency_id: "ARS",
|
||||
unit_price: unitPrice,
|
||||
},
|
||||
],
|
||||
notification_url: notificationUrl,
|
||||
...(payer ? { payer } : {}),
|
||||
};
|
||||
|
||||
const data = await fetchMp({
|
||||
url: "https://api.mercadopago.com/checkout/preferences",
|
||||
method: "POST",
|
||||
body: payload,
|
||||
});
|
||||
|
||||
return {
|
||||
preference_id: data?.id || null,
|
||||
init_point: data?.init_point || null,
|
||||
sandbox_init_point: data?.sandbox_init_point || null,
|
||||
raw: data,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSignatureHeader(header) {
|
||||
const h = String(header || "");
|
||||
const parts = h.split(",");
|
||||
let ts = null;
|
||||
let v1 = null;
|
||||
for (const p of parts) {
|
||||
const [k, v] = p.split("=");
|
||||
if (!k || !v) continue;
|
||||
const key = k.trim();
|
||||
const val = v.trim();
|
||||
if (key === "ts") ts = val;
|
||||
if (key === "v1") v1 = val;
|
||||
}
|
||||
return { ts, v1 };
|
||||
}
|
||||
|
||||
export function verifyWebhookSignature({ headers = {}, query = {} }) {
|
||||
const secret = getWebhookSecret();
|
||||
if (!secret) return { ok: false, reason: "MP_WEBHOOK_SECRET is not set" };
|
||||
const xSignature = headers["x-signature"] || headers["X-Signature"] || headers["x-signature"];
|
||||
const xRequestId = headers["x-request-id"] || headers["X-Request-Id"] || headers["x-request-id"];
|
||||
const { ts, v1 } = parseSignatureHeader(xSignature);
|
||||
const dataId = query["data.id"] || query?.data?.id || null;
|
||||
if (!xRequestId || !ts || !v1 || !dataId) {
|
||||
return { ok: false, reason: "missing_signature_fields" };
|
||||
}
|
||||
const manifest = `id:${String(dataId).toLowerCase()};request-id:${xRequestId};ts:${ts};`;
|
||||
const hmac = crypto.createHmac("sha256", secret);
|
||||
hmac.update(manifest);
|
||||
const hash = hmac.digest("hex");
|
||||
const ok = crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(v1));
|
||||
return ok ? { ok: true } : { ok: false, reason: "invalid_signature" };
|
||||
}
|
||||
|
||||
export async function fetchPayment({ paymentId }) {
|
||||
if (!paymentId) throw new Error("missing_payment_id");
|
||||
return await fetchMp({
|
||||
url: `https://api.mercadopago.com/v1/payments/${encodeURIComponent(paymentId)}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
export function parseExternalReference(externalReference) {
|
||||
if (!externalReference) return { tenantId: null, wooOrderId: null };
|
||||
const parts = String(externalReference).split("|").filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
return { tenantId: parts[0], wooOrderId: Number(parts[1]) || null };
|
||||
}
|
||||
return { tenantId: null, wooOrderId: Number(externalReference) || null };
|
||||
}
|
||||
|
||||
export async function reconcilePayment({ tenantId, payment }) {
|
||||
const status = payment?.status || null;
|
||||
const paidAt = payment?.date_approved || payment?.date_created || null;
|
||||
const { tenantId: refTenantId, wooOrderId } = parseExternalReference(payment?.external_reference);
|
||||
const resolvedTenantId = tenantId || refTenantId;
|
||||
if (!resolvedTenantId) throw new Error("tenant_id_missing_from_payment");
|
||||
const saved = await upsertMpPayment({
|
||||
tenant_id: resolvedTenantId,
|
||||
woo_order_id: wooOrderId,
|
||||
preference_id: payment?.order?.id || payment?.preference_id || null,
|
||||
payment_id: String(payment?.id || ""),
|
||||
status,
|
||||
paid_at: paidAt,
|
||||
raw: payment,
|
||||
});
|
||||
|
||||
if (status === "approved" && wooOrderId) {
|
||||
await updateOrderStatus({ tenantId: resolvedTenantId, wooOrderId, status: "processing" });
|
||||
}
|
||||
|
||||
return { payment: saved, woo_order_id: wooOrderId, tenant_id: resolvedTenantId };
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import express from "express";
|
||||
import { makeMercadoPagoReturn, makeMercadoPagoWebhook } from "../controllers/mercadoPago.js";
|
||||
|
||||
export function createMercadoPagoRouter() {
|
||||
const router = express.Router();
|
||||
router.post("/webhook/mercadopago", makeMercadoPagoWebhook());
|
||||
router.get("/return", makeMercadoPagoReturn());
|
||||
return router;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user