Actualizar server.js
This commit is contained in:
468
server.js
468
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user