Actualizar server.js
This commit is contained in:
135
server.js
135
server.js
@@ -5,6 +5,7 @@ import jwt from "jsonwebtoken";
|
||||
import pg from "pg";
|
||||
import crypto from "crypto";
|
||||
import OpenAI from "openai";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const { Pool } = pg;
|
||||
const app = express();
|
||||
@@ -4233,26 +4234,128 @@ app.post("/public/portal/:token/budget/:id/respond", async (req, res) => {
|
||||
[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]);
|
||||
const bQ = await pool.query("SELECT client_name, total FROM budgets WHERE id = $1", [id]);
|
||||
// ==========================================
|
||||
// 💳 6. MOTOR DE PAGOS (STRIPE) PARA PRESUPUESTOS Y FACTURAS
|
||||
// ==========================================
|
||||
|
||||
// A) CREAR SESIÓN DE PAGO (Cuando el cliente pulsa "Pagar")
|
||||
app.post("/public/portal/:token/budget/:id/checkout", async (req, res) => {
|
||||
try {
|
||||
const { token, id } = req.params;
|
||||
|
||||
// 1. Identificar al cliente y su empresa (dueño)
|
||||
const clientQ = await pool.query("SELECT owner_id, full_name, phone FROM clients WHERE portal_token = $1", [token]);
|
||||
if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Token inválido" });
|
||||
const ownerId = clientQ.rows[0].owner_id;
|
||||
|
||||
// 2. Extraer datos del presupuesto
|
||||
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 (ownerData.rowCount > 0 && bQ.rowCount > 0) {
|
||||
const adminPhone = ownerData.rows[0].phone;
|
||||
const bInfo = bQ.rows[0];
|
||||
|
||||
const msgWa = action === 'accept'
|
||||
? `🟢 *PRESUPUESTO ACEPTADO*\nEl cliente ${bInfo.client_name} ha ACEPTADO y firmado el presupuesto PRE-${id} por ${bInfo.total}€.`
|
||||
: `🔴 *PRESUPUESTO RECHAZADO*\nEl cliente ${bInfo.client_name} ha RECHAZADO el presupuesto PRE-${id}.`;
|
||||
|
||||
// Lo enviamos a tu número usando la instancia de tu empresa
|
||||
sendWhatsAppAuto(adminPhone, msgWa, `cliente_${ownerId}`, false).catch(console.error);
|
||||
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) {
|
||||
console.error("Error firmando presupuesto:", e);
|
||||
res.status(500).json({ ok: false });
|
||||
console.error("❌ Error creando sesión de Stripe:", e);
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user