Actualizar server.js

This commit is contained in:
2026-03-29 15:24:02 +00:00
parent 2a31489f9c
commit c9e9083707

131
server.js
View File

@@ -5,6 +5,7 @@ import jwt from "jsonwebtoken";
import pg from "pg"; import pg from "pg";
import crypto from "crypto"; import crypto from "crypto";
import OpenAI from "openai"; import OpenAI from "openai";
import Stripe from "stripe";
const { Pool } = pg; const { Pool } = pg;
const app = express(); const app = express();
@@ -4233,26 +4234,128 @@ app.post("/public/portal/:token/budget/:id/respond", async (req, res) => {
[newStatus, signature || null, id, ownerId] [newStatus, signature || null, id, ownerId]
); );
// 4. Avisar a la OFICINA (Admin) por WhatsApp del resultado // ==========================================
const ownerData = await pool.query("SELECT phone FROM users WHERE id = $1", [ownerId]); // 💳 6. MOTOR DE PAGOS (STRIPE) PARA PRESUPUESTOS Y FACTURAS
const bQ = await pool.query("SELECT client_name, total FROM budgets WHERE id = $1", [id]); // ==========================================
if (ownerData.rowCount > 0 && bQ.rowCount > 0) { // A) CREAR SESIÓN DE PAGO (Cuando el cliente pulsa "Pagar")
const adminPhone = ownerData.rows[0].phone; app.post("/public/portal/:token/budget/:id/checkout", async (req, res) => {
const bInfo = bQ.rows[0]; try {
const { token, id } = req.params;
const msgWa = action === 'accept' // 1. Identificar al cliente y su empresa (dueño)
? `🟢 *PRESUPUESTO ACEPTADO*\nEl cliente ${bInfo.client_name} ha ACEPTADO y firmado el presupuesto PRE-${id} por ${bInfo.total}€.` const clientQ = await pool.query("SELECT owner_id, full_name, phone FROM clients WHERE portal_token = $1", [token]);
: `🔴 *PRESUPUESTO RECHAZADO*\nEl cliente ${bInfo.client_name} ha RECHAZADO el presupuesto PRE-${id}.`; if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Token inválido" });
const ownerId = clientQ.rows[0].owner_id;
// Lo enviamos a tu número usando la instancia de tu empresa // 2. Extraer datos del presupuesto
sendWhatsAppAuto(adminPhone, msgWa, `cliente_${ownerId}`, false).catch(console.error); const bQ = await pool.query("SELECT * FROM budgets WHERE id = $1 AND owner_id = $2", [id, ownerId]);
if (bQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Presupuesto no encontrado" });
const budget = bQ.rows[0];
// 3. Extraer las claves secretas de Stripe de ESA EMPRESA EN CONCRETO (Modo SaaS)
const ownerQ = await pool.query("SELECT billing_settings FROM users WHERE id = $1", [ownerId]);
const billingSettings = ownerQ.rows[0]?.billing_settings || {};
if (!billingSettings.stripe_enabled || !billingSettings.stripe_sk) {
return res.status(400).json({ ok: false, error: "Los pagos con tarjeta no están habilitados para esta empresa." });
} }
res.json({ ok: true }); // 4. Iniciar Stripe de forma dinámica con la llave del cliente
const stripe = new Stripe(billingSettings.stripe_sk, { apiVersion: "2023-10-16" });
// 5. Crear la ventana mágica de pago (Checkout Session)
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
customer_email: budget.client_email || undefined, // Opcional, si tienes su email
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: `Presupuesto #PRE-${budget.id}`,
description: `Servicios de asistencia técnica para ${budget.client_name}`,
},
unit_amount: Math.round(budget.total * 100), // Stripe cobra en céntimos (84.70€ = 8470)
},
quantity: 1,
},
],
mode: 'payment',
// Enviaremos metadatos para saber exactamente qué han pagado cuando Stripe nos avise de vuelta
metadata: {
budget_id: budget.id,
owner_id: ownerId,
client_phone: budget.client_phone
},
// Redirecciones tras el pago
success_url: `https://portal.integrarepara.es/pago_exito.html?budget_id=${budget.id}`,
cancel_url: `https://portal.integrarepara.es/?token=${token}`,
});
// 6. Devolvemos el link seguro de Stripe al frontend para que redirija al cliente
res.json({ ok: true, checkout_url: session.url });
} catch (e) { } catch (e) {
console.error("Error firmando presupuesto:", e); console.error("Error creando sesión de Stripe:", e);
res.status(500).json({ ok: false }); res.status(500).json({ ok: false, error: e.message });
}
});
// B) WEBHOOK DE STRIPE (El chivatazo invisible que avisa cuando el cliente YA ha pagado)
// Nota: Stripe necesita que el body llegue "crudo" para verificar las firmas, por eso le quitamos el express.json
app.post("/webhook/stripe", express.raw({ type: 'application/json' }), async (req, res) => {
try {
const sig = req.headers['stripe-signature'];
const body = req.body;
// Por ahora, como es un entorno SaaS complejo, procesaremos el evento de forma directa
// En producción, es altamente recomendable verificar el 'endpoint_secret' de cada webhook
const event = JSON.parse(body);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Extraer la "matrícula" oculta que le pusimos al pago
const budgetId = session.metadata.budget_id;
const ownerId = session.metadata.owner_id;
const amountTotal = (session.amount_total / 100).toFixed(2); // De céntimos a Euros
console.log(`💰 [STRIPE WEBHOOK] ¡PAGO RECIBIDO! Presupuesto PRE-${budgetId} por ${amountTotal}€ (Owner: ${ownerId})`);
// 1. Marcar el presupuesto como "Convertido/Pagado"
await pool.query("UPDATE budgets SET status = 'converted' WHERE id = $1 AND owner_id = $2", [budgetId, ownerId]);
// 2. Si ya existía un servicio asociado, marcarlo en contabilidad
const sq = await pool.query("SELECT id FROM scraped_services WHERE service_ref = $1 AND owner_id = $2", [`PRE-${budgetId}`, ownerId]);
if (sq.rowCount > 0) {
const serviceId = sq.rows[0].id;
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]);
// 3. Trazabilidad
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.`]
);
}
// 4. ¡Avisar al jefe por WhatsApp!
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}`);
} }
}); });