From b835eaeda64d6b52aca8f8fd3282f8912566ba7a Mon Sep 17 00:00:00 2001 From: marsalva Date: Fri, 3 Apr 2026 21:35:40 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 468 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) diff --git a/server.js b/server.js index e07adb8..c0d793f 100644 --- a/server.js +++ b/server.js @@ -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;