Actualizar server.js

This commit is contained in:
2026-04-03 21:35:40 +00:00
parent 9c538f7a7a
commit b835eaeda6

468
server.js
View File

@@ -33,6 +33,411 @@ app.use(cors(corsOptions));
// Habilitar pre-flight para todas las rutas
app.options('*', cors(corsOptions));
// ==========================================
// 💳 WEBHOOK STRIPE (DEBE IR ANTES DE express.json)
// ==========================================
app.post("/webhook/stripe", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
if (!STRIPE_WEBHOOK_SECRET) {
console.error("❌ STRIPE_WEBHOOK_SECRET no configurado.");
return res.status(500).send("Webhook secret no configurado");
}
const signature = req.headers["stripe-signature"];
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "sk_test_dummy", { apiVersion: "2023-10-16" });
event = stripe.webhooks.constructEvent(req.body, signature, STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error("❌ Firma webhook Stripe inválida:", err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
const insertarEventoPago = async ({
subscriptionId = null,
companyId = null,
stripeInvoiceId = null,
stripePaymentIntentId = null,
stripeCheckoutSessionId = null,
stripeEventId = null,
amount = 0,
currency = "eur",
status = "pendiente",
eventType = null,
paidAt = null
}) => {
try {
await pool.query(`
INSERT INTO protection_payment_events (
subscription_id,
company_id,
stripe_invoice_id,
stripe_payment_intent_id,
stripe_checkout_session_id,
stripe_event_id,
amount,
currency,
status,
event_type,
paid_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
`, [
subscriptionId,
companyId,
stripeInvoiceId,
stripePaymentIntentId,
stripeCheckoutSessionId,
stripeEventId,
amount,
currency,
status,
eventType,
paidAt
]);
} catch (e) {
console.error("⚠️ Error insertando protection_payment_events:", e.message);
}
};
try {
switch (event.type) {
// =====================================================
// 1. CHECKOUT COMPLETADO
// =====================================================
case "checkout.session.completed": {
const session = event.data.object;
const metadata = session.metadata || {};
const paymentType = metadata.type;
// 🟢 PLAN DE PROTECCIÓN
if (paymentType === "protection_plan") {
const subscriptionId = parseInt(metadata.subscription_id, 10);
const ownerId = parseInt(metadata.owner_id, 10);
if (!subscriptionId || !ownerId) {
console.warn("⚠️ checkout.session.completed sin metadata válida para protection_plan");
break;
}
await pool.query(`
UPDATE protection_subscriptions
SET
payment_status = 'pagado',
status = 'activo',
stripe_customer_id = COALESCE($1, stripe_customer_id),
stripe_subscription_id = COALESCE($2, stripe_subscription_id),
started_at = COALESCE(started_at, NOW()),
last_payment_at = NOW(),
updated_at = NOW()
WHERE id = $3 AND company_id = $4
`, [
session.customer ? String(session.customer) : null,
session.subscription ? String(session.subscription) : null,
subscriptionId,
ownerId
]);
await pool.query(`
INSERT INTO protection_activity (company_id, type, description)
VALUES ($1, 'cobro', $2)
`, [
ownerId,
`Alta confirmada por Stripe. Suscripción #${subscriptionId} activada correctamente.`
]);
await insertarEventoPago({
subscriptionId,
companyId: ownerId,
stripeCheckoutSessionId: session.id || null,
stripePaymentIntentId: session.payment_intent || null,
stripeEventId: event.id,
amount: ((session.amount_total || 0) / 100),
currency: session.currency || "eur",
status: "paid",
eventType: event.type,
paidAt: new Date()
});
console.log(`✅ [STRIPE] Alta plan activada. Sub ID ${subscriptionId}`);
}
// 🔵 PRESUPUESTO NORMAL
else {
const budgetId = session.metadata?.budget_id;
const ownerId = session.metadata?.owner_id;
const amountTotal = ((session.amount_total || 0) / 100).toFixed(2);
if (!budgetId || !ownerId) {
console.warn("⚠️ checkout.session.completed de presupuesto sin metadata suficiente");
break;
}
await pool.query(
"UPDATE budgets SET status = 'paid' WHERE id = $1 AND owner_id = $2",
[budgetId, ownerId]
);
const sq = await pool.query(`
SELECT id, raw_data
FROM scraped_services
WHERE service_ref = $1 AND owner_id = $2
`, [`PRE-${budgetId}`, ownerId]);
if (sq.rowCount > 0) {
const serviceId = sq.rows[0].id;
let rawData = sq.rows[0].raw_data || {};
rawData.is_paid = true;
await pool.query(
"UPDATE scraped_services SET raw_data = $1 WHERE id = $2",
[JSON.stringify(rawData), serviceId]
);
await pool.query(`
INSERT INTO service_financials (scraped_id, amount, payment_method, is_paid)
VALUES ($1, $2, 'Tarjeta (Stripe)', true)
ON CONFLICT (scraped_id)
DO UPDATE SET
amount = EXCLUDED.amount,
is_paid = true,
payment_method = 'Tarjeta (Stripe)',
updated_at = NOW()
`, [serviceId, amountTotal]);
await pool.query(`
INSERT INTO scraped_service_logs (scraped_id, user_name, action, details)
VALUES ($1, $2, $3, $4)
`, [
serviceId,
"Stripe API",
"Pago Confirmado",
`El cliente ha abonado ${amountTotal}€ por pasarela segura.`
]);
}
const ownerQ = await pool.query("SELECT phone FROM users WHERE id = $1", [ownerId]);
if (ownerQ.rowCount > 0) {
const msgWa = `💰 *¡PAGO RECIBIDO (STRIPE)!*\n\nSe acaba de confirmar el pago con tarjeta del presupuesto *PRE-${budgetId}* por un importe de *${amountTotal}€*.\n\nEl sistema lo ha marcado como pagado automáticamente.`;
sendWhatsAppAuto(ownerQ.rows[0].phone, msgWa, `cliente_${ownerId}`, false).catch(console.error);
}
console.log(`✅ [STRIPE] Presupuesto PRE-${budgetId} pagado`);
}
break;
}
// =====================================================
// 2. FACTURA PAGADA (RENOVACIONES Y COBROS REALES)
// =====================================================
case "invoice.paid": {
const invoice = event.data.object;
const stripeSubscriptionId = invoice.subscription ? String(invoice.subscription) : null;
if (!stripeSubscriptionId) break;
const subQ = await pool.query(`
SELECT id, company_id
FROM protection_subscriptions
WHERE stripe_subscription_id = $1
ORDER BY created_at DESC
LIMIT 1
`, [stripeSubscriptionId]);
if (subQ.rowCount === 0) {
console.warn(`⚠️ invoice.paid sin subscription local para ${stripeSubscriptionId}`);
break;
}
const localSub = subQ.rows[0];
const periodStartUnix =
invoice.lines?.data?.[0]?.period?.start || null;
const periodEndUnix =
invoice.lines?.data?.[0]?.period?.end || null;
await pool.query(`
UPDATE protection_subscriptions
SET
payment_status = 'pagado',
status = 'activo',
stripe_customer_id = COALESCE($1, stripe_customer_id),
current_period_start = COALESCE(to_timestamp($2), current_period_start),
current_period_end = COALESCE(to_timestamp($3), current_period_end),
renewal_date = CASE
WHEN $3 IS NOT NULL THEN to_timestamp($3)::date
ELSE renewal_date
END,
last_payment_at = NOW(),
updated_at = NOW()
WHERE id = $4
`, [
invoice.customer ? String(invoice.customer) : null,
periodStartUnix,
periodEndUnix,
localSub.id
]);
await pool.query(`
INSERT INTO protection_activity (company_id, type, description)
VALUES ($1, 'cobro', $2)
`, [
localSub.company_id,
`Cobro mensual confirmado por Stripe para la suscripción #${localSub.id}.`
]);
await insertarEventoPago({
subscriptionId: localSub.id,
companyId: localSub.company_id,
stripeInvoiceId: invoice.id || null,
stripePaymentIntentId: invoice.payment_intent || null,
stripeEventId: event.id,
amount: ((invoice.amount_paid || 0) / 100),
currency: invoice.currency || "eur",
status: "paid",
eventType: event.type,
paidAt: new Date()
});
console.log(`✅ [STRIPE] invoice.paid procesado para sub local ${localSub.id}`);
break;
}
// =====================================================
// 3. FACTURA FALLIDA
// =====================================================
case "invoice.payment_failed": {
const invoice = event.data.object;
const stripeSubscriptionId = invoice.subscription ? String(invoice.subscription) : null;
if (!stripeSubscriptionId) break;
const subQ = await pool.query(`
SELECT id, company_id
FROM protection_subscriptions
WHERE stripe_subscription_id = $1
ORDER BY created_at DESC
LIMIT 1
`, [stripeSubscriptionId]);
if (subQ.rowCount === 0) break;
const localSub = subQ.rows[0];
await pool.query(`
UPDATE protection_subscriptions
SET
payment_status = 'impagado',
status = 'suspendido',
updated_at = NOW()
WHERE id = $1
`, [localSub.id]);
await pool.query(`
INSERT INTO protection_activity (company_id, type, description)
VALUES ($1, 'cobro', $2)
`, [
localSub.company_id,
`Pago fallido detectado por Stripe en la suscripción #${localSub.id}.`
]);
await insertarEventoPago({
subscriptionId: localSub.id,
companyId: localSub.company_id,
stripeInvoiceId: invoice.id || null,
stripePaymentIntentId: invoice.payment_intent || null,
stripeEventId: event.id,
amount: ((invoice.amount_due || 0) / 100),
currency: invoice.currency || "eur",
status: "failed",
eventType: event.type,
paidAt: null
});
console.log(`⚠️ [STRIPE] invoice.payment_failed para sub local ${localSub.id}`);
break;
}
// =====================================================
// 4. SUSCRIPCIÓN ACTUALIZADA
// =====================================================
case "customer.subscription.updated": {
const subscription = event.data.object;
const stripeSubscriptionId = subscription.id ? String(subscription.id) : null;
if (!stripeSubscriptionId) break;
await pool.query(`
UPDATE protection_subscriptions
SET
stripe_customer_id = COALESCE($1, stripe_customer_id),
current_period_start = COALESCE(to_timestamp($2), current_period_start),
current_period_end = COALESCE(to_timestamp($3), current_period_end),
renewal_date = CASE
WHEN $3 IS NOT NULL THEN to_timestamp($3)::date
ELSE renewal_date
END,
cancel_at_period_end = COALESCE($4, cancel_at_period_end),
status = CASE
WHEN $5 = 'active' THEN 'activo'
WHEN $5 IN ('past_due', 'unpaid') THEN 'suspendido'
WHEN $5 IN ('canceled', 'incomplete_expired') THEN 'cancelado'
ELSE status
END,
payment_status = CASE
WHEN $5 = 'active' THEN 'pagado'
WHEN $5 IN ('past_due', 'unpaid') THEN 'impagado'
ELSE payment_status
END,
updated_at = NOW()
WHERE stripe_subscription_id = $6
`, [
subscription.customer ? String(subscription.customer) : null,
subscription.current_period_start || null,
subscription.current_period_end || null,
subscription.cancel_at_period_end ?? false,
subscription.status || null,
stripeSubscriptionId
]);
console.log(`🔄 [STRIPE] customer.subscription.updated ${stripeSubscriptionId}`);
break;
}
// =====================================================
// 5. SUSCRIPCIÓN CANCELADA / TERMINADA
// =====================================================
case "customer.subscription.deleted": {
const subscription = event.data.object;
const stripeSubscriptionId = subscription.id ? String(subscription.id) : null;
if (!stripeSubscriptionId) break;
await pool.query(`
UPDATE protection_subscriptions
SET
status = 'cancelado',
cancel_at_period_end = false,
cancelled_at = NOW(),
ended_at = NOW(),
updated_at = NOW()
WHERE stripe_subscription_id = $1
`, [stripeSubscriptionId]);
console.log(`🛑 [STRIPE] customer.subscription.deleted ${stripeSubscriptionId}`);
break;
}
default:
console.log(` Evento Stripe ignorado: ${event.type}`);
break;
}
return res.json({ received: true });
} catch (e) {
console.error("❌ Error grave procesando Webhook de Stripe:", e.message);
return res.status(500).send(`Webhook Error: ${e.message}`);
}
});
// Límites de subida para logotipos en Base64
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ limit: '10mb', extended: true }));
@@ -47,6 +452,8 @@ const {
EVOLUTION_INSTANCE,
OPENAI_API_KEY, // 🔔 LEER LLAVE
OPENAI_MODEL // 🔔 LEER MODELO
STRIPE_WEBHOOK_SECRET
} = process.env;
// --- 2. INICIALIZACIÓN GLOBAL DEL MOTOR IA (ESTO ES LO QUE TE FALTABA) ---
@@ -59,6 +466,12 @@ console.log("------------------------------------------------");
console.log("🚀 VERSIÓN COMPLETA - INTEGRA REPARA SAAS");
console.log("------------------------------------------------");
if (!STRIPE_WEBHOOK_SECRET) {
console.error("⚠️ AVISO: Falta STRIPE_WEBHOOK_SECRET en variables de entorno.");
} else {
console.log("✅ Stripe Webhook Secret detectado.");
}
if (!OPENAI_API_KEY) {
console.error("⚠️ AVISO: Falta OPENAI_API_KEY en variables de entorno.");
} else {
@@ -4856,6 +5269,61 @@ app.post("/public/portal/:token/budget/:id/checkout", async (req, res) => {
// B) WEBHOOK DE STRIPE (El chivatazo invisible que avisa cuando el cliente YA ha pagado)
app.post("/webhook/stripe", async (req, res) => {
try {
const event = req.body;
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const ownerId = session.metadata.owner_id;
const amountTotal = (session.amount_total / 100).toFixed(2);
const paymentType = session.metadata.type;
if (paymentType === 'protection_plan') {
const subId = session.metadata.subscription_id;
console.log(`💰 [STRIPE WEBHOOK] Pago de Seguro PREM-${subId} por ${amountTotal}`);
await pool.query("UPDATE protection_subscriptions SET payment_status = 'pagado', status = 'activo' WHERE id = $1 AND company_id = $2", [subId, ownerId]);
await pool.query("INSERT INTO protection_activity (company_id, type, description) VALUES ($1, 'cobro', $2)", [ownerId, `Pago de suscripción inicial confirmado (${amountTotal}€)`]);
} else {
const budgetId = session.metadata.budget_id;
console.log(`💰 [STRIPE WEBHOOK] ¡PAGO RECIBIDO! Presupuesto PRE-${budgetId} por ${amountTotal}`);
await pool.query("UPDATE budgets SET status = 'paid' WHERE id = $1 AND owner_id = $2", [budgetId, ownerId]);
const sq = await pool.query("SELECT id, raw_data FROM scraped_services WHERE service_ref = $1 AND owner_id = $2", [`PRE-${budgetId}`, ownerId]);
if (sq.rowCount > 0) {
const serviceId = sq.rows[0].id;
let rawData = sq.rows[0].raw_data || {};
rawData.is_paid = true;
await pool.query("UPDATE scraped_services SET raw_data = $1 WHERE id = $2", [JSON.stringify(rawData), serviceId]);
await pool.query(`
INSERT INTO service_financials (scraped_id, amount, payment_method, is_paid)
VALUES ($1, $2, 'Tarjeta (Stripe)', true)
ON CONFLICT (scraped_id) DO UPDATE SET is_paid = true, payment_method = 'Tarjeta (Stripe)'
`, [serviceId, amountTotal]);
await pool.query("INSERT INTO scraped_service_logs (scraped_id, user_name, action, details) VALUES ($1, $2, $3, $4)",
[serviceId, "Stripe API", "Pago Confirmado", `El cliente ha abonado ${amountTotal}€ por pasarela segura.`]
);
}
const ownerQ = await pool.query("SELECT phone FROM users WHERE id = $1", [ownerId]);
if (ownerQ.rowCount > 0) {
const msgWa = `💰 *¡PAGO RECIBIDO (STRIPE)!*\n\nSe acaba de confirmar el pago con tarjeta del presupuesto *PRE-${budgetId}* por un importe de *${amountTotal}€*.\n\nEl sistema lo ha marcado como pagado automáticamente.`;
sendWhatsAppAuto(ownerQ.rows[0].phone, msgWa, `cliente_${ownerId}`, false).catch(console.error);
}
}
}
res.json({ received: true });
} catch (e) {
console.error("❌ Error grave procesando Webhook de Stripe:", e.message);
res.status(400).send(`Webhook Error: ${e.message}`);
}
});
try {
const event = req.body;