const Stripe = require("stripe"); const Payment = require("../models/payment.module"); const User = require("../models/user.model"); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2022-11-15", }); /* ----------------------------------------------- PLAN → PRICE ID MAPPING ------------------------------------------------ */ const PLAN_PRICE_MAP = { basic_monthly: process.env.STRIPE_PRICE_SB_BASIC_MONTHLY, standard_monthly: process.env.STRIPE_PRICE_SB_STANDARD_MONTHLY, premium_monthly: process.env.STRIPE_PRICE_SB_PREMIUM_MONTHLY, basic_yearly: process.env.STRIPE_PRICE_SB_BASIC_YEARLY, standard_yearly: process.env.STRIPE_PRICE_SB_STANDARD_YEARLY, premium_yearly: process.env.STRIPE_PRICE_SB_PREMIUM_YEARLY, }; /* ----------------------------------------------------- CREATE CHECKOUT SESSION — SUBSCRIPTIONS ------------------------------------------------------ */ async function createCheckoutSession(req, res) { try { const { email, planId, userId } = req.body; if (!email || !planId || !userId) { return res.status(400).json({ error: "email, planId & userId required" }); } const priceId = PLAN_PRICE_MAP[planId]; if (!priceId) return res.status(400).json({ error: "Invalid planId" }); const price = await stripe.prices.retrieve(priceId); const session = await stripe.checkout.sessions.create({ mode: "subscription", customer_email: email, line_items: [{ price: priceId, quantity: 1 }], success_url: `${process.env.FRONTEND_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.FRONTEND_URL}/payment/cancel`, metadata: { email, planId, userId }, }); await Payment.create({ userId, email, planId, amount: price.unit_amount || 0, stripeSessionId: session.id, status: "pending", }); res.json({ url: session.url }); } catch (err) { console.error("Checkout Error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- PAYMENT INTENT (ONE-TIME ADD-ON) ------------------------------------------------------ */ async function createPaymentIntent(req, res) { try { const { email, planId, userId } = req.body; if (!email || !planId || !userId) { return res.status(400).json({ error: "email, planId & userId required" }); } const priceId = PLAN_PRICE_MAP[planId]; if (!priceId) return res.status(400).json({ error: "Invalid planId" }); const price = await stripe.prices.retrieve(priceId); const paymentIntent = await stripe.paymentIntents.create({ amount: price.unit_amount, currency: price.currency, metadata: { email, planId, userId }, automatic_payment_methods: { enabled: true }, }); await Payment.create({ userId, email, amount: price.unit_amount, planId, stripePaymentIntentId: paymentIntent.id, status: "pending", }); res.json({ clientSecret: paymentIntent.client_secret }); } catch (err) { console.error("PaymentIntent Error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- STRIPE WEBHOOK HANDLER ------------------------------------------------------ */ async function handleWebhook(req, res) { let event; try { const signature = req.headers["stripe-signature"]; event = stripe.webhooks.constructEvent( req.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } switch (event.type) { /* ------------------------- CHECKOUT SUCCESS ---------------------------- */ case "checkout.session.completed": { const session = event.data.object; const customerId = session.customer; // Fetch subscription details from Stripe const subscription = await stripe.subscriptions.retrieve( session.subscription ); // Update Payment Record const updatedPayment = await Payment.findOneAndUpdate( { stripeSessionId: session.id }, { subscriptionId: session.subscription, status: "active", subscriptionStartDate: new Date(subscription.current_period_start * 1000), subscriptionEndDate: new Date(subscription.current_period_end * 1000), }, { new: true } ); // Update User Record with Stripe Customer ID if (updatedPayment && updatedPayment.userId && customerId) { await User.findByIdAndUpdate(updatedPayment.userId, { stripeCustomerId: customerId }); } break; } /* ------------------------- PAYMENT FAILED ---------------------------- */ case "invoice.payment_failed": await Payment.findOneAndUpdate( { subscriptionId: event.data.object.subscription }, { status: "failed" } ); break; /* ------------------------- SUBSCRIPTION CANCELLED ---------------------------- */ case "customer.subscription.deleted": { const sub = event.data.object; await Payment.findOneAndUpdate( { subscriptionId: sub.id }, { status: "canceled", subscriptionEndDate: new Date(sub.canceled_at * 1000), } ); break; } } res.json({ received: true }); } /* ----------------------------------------------------- CANCEL SUBSCRIPTION (MANUAL CANCEL FROM UI) ------------------------------------------------------ */ async function cancelSubscription(req, res) { try { const { session_id } = req.body; if (!session_id) return res.status(400).json({ error: "session_id required" }); const payment = await Payment.findOne({ stripeSessionId: session_id }); if (!payment) return res.status(404).json({ error: "Subscription not found" }); // If it's a Stripe subscription if (payment.subscriptionId && payment.planId !== "free_trial") { const canceledSub = await stripe.subscriptions.cancel(payment.subscriptionId); await Payment.findOneAndUpdate( { stripeSessionId: session_id }, { status: "canceled", subscriptionEndDate: new Date(canceledSub.canceled_at * 1000), } ); } // If it's a Free Trial (Mock) else if (payment.planId === "free_trial") { await Payment.findOneAndUpdate( { stripeSessionId: session_id }, { status: "canceled", subscriptionEndDate: new Date(), // End immediately } ); // Also update User model await User.findByIdAndUpdate(payment.userId, { isTrialActive: false, trialEndsAt: new Date() // Expire immediately }); } res.json({ message: "Subscription/Trial cancelled successfully" }); } catch (err) { console.error("Cancel subscription error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- GET PAYMENT DETAILS ------------------------------------------------------ */ async function getPaymentDetails(req, res) { try { const { session_id } = req.query; if (!session_id) return res.status(400).json({ error: "session_id required" }); const payment = await Payment.findOne({ stripeSessionId: session_id }); if (!payment) return res.status(404).json({ error: "Payment not found" }); res.json({ success: true, message: "Payment details fetched successfully", data: { id: payment._id, userId: payment.userId, email: payment.email, planId: payment.planId, amount: payment.amount / 100 || 0, status: payment.status, stripeSessionId: payment.stripeSessionId, subscriptionId: payment.subscriptionId, subscriptionStartDate: payment.subscriptionStartDate, subscriptionEndDate: payment.subscriptionEndDate, createdAt: payment.createdAt, }, }); } catch (err) { console.error("Payment details fetch error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- ACTIVATE FREE TRIAL (7 DAYS) ------------------------------------------------------ */ async function activateTrial(req, res) { try { const { userId, email } = req.body; if (!userId || !email) { return res.status(400).json({ error: "userId & email required" }); } // Check if user already had a trial const user = await User.findById(userId); if (!user) return res.status(404).json({ error: "User not found" }); // If user has already used trial logic (if you want to restrict it once) // For now, let's assume if they have a 'free_trial' payment record, they can't do it again. const existingTrial = await Payment.findOne({ userId, planId: "free_trial" }); if (existingTrial) { return res.status(400).json({ error: "Trial already activated previously." }); } // Create 7-day trial dates const startDate = new Date(); const endDate = new Date(); endDate.setDate(startDate.getDate() + 7); // Create Payment Record (dummy so frontend sees it as active subscription) const payment = await Payment.create({ userId, email, planId: "free_trial", amount: 0, status: "active", subscriptionStartDate: startDate, subscriptionEndDate: endDate, stripeSessionId: `trial_${userId}_${Date.now()}` // Mock ID }); // Update User model user.trialEndsAt = endDate; user.isTrialActive = true; await user.save(); res.json({ success: true, message: "7-day free trial activated!", payment: { id: payment._id, planId: payment.planId, amount: payment.amount, status: payment.status, sessionId: payment.stripeSessionId, createdAt: payment.createdAt } }); } catch (err) { console.error("Activate trial error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- GET USER SUBSCRIPTION STATUS (For Pricing Page) ------------------------------------------------------ */ async function getUserSubscriptionStatus(req, res) { try { const { userId } = req.query; if (!userId) return res.status(400).json({ error: "userId required" }); // Check if user ever had a free trial const trialRecord = await Payment.findOne({ userId, planId: "free_trial" }); const hasUsedTrial = !!trialRecord; // Check for currently active subscription (Trial or Paid) // We look for status 'active' and end date in the future const activeSub = await Payment.findOne({ userId, status: "active", subscriptionEndDate: { $gt: new Date() } }).sort({ createdAt: -1 }); let isTrialActive = false; let trialEndsAt = null; let currentPlan = null; let stripeSessionId = null; if (activeSub) { currentPlan = activeSub.planId; stripeSessionId = activeSub.stripeSessionId; if (activeSub.planId === "free_trial") { isTrialActive = true; trialEndsAt = activeSub.subscriptionEndDate; } } res.json({ hasUsedTrial, isTrialActive, trialStartDate: trialRecord ? trialRecord.subscriptionStartDate : null, trialEndsAt, currentPlan, stripeSessionId }); } catch (err) { console.error("Get sub status error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- CREATE CUSTOMER PORTAL SESSION (Manage Billing) ------------------------------------------------------ */ async function createPortalSession(req, res) { try { const { userId } = req.body; if (!userId) return res.status(400).json({ error: "userId required" }); const user = await User.findById(userId); if (!user || !user.stripeCustomerId) { return res.status(404).json({ error: "No Stripe customer found for this user" }); } const session = await stripe.billingPortal.sessions.create({ customer: user.stripeCustomerId, return_url: `${process.env.FRONTEND_URL}/account-settings`, }); res.json({ url: session.url }); } catch (err) { console.error("Portal Session Error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- GET BILLING INFO (Last 4 digits) ------------------------------------------------------ */ async function getBillingInfo(req, res) { try { const { userId } = req.query; if (!userId) return res.status(400).json({ error: "userId required" }); const user = await User.findById(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!user.stripeCustomerId) { return res.json({ hasPaymentMethod: false }); } // List payment methods const paymentMethods = await stripe.customers.listPaymentMethods( user.stripeCustomerId, { type: 'card' } ); if (paymentMethods.data && paymentMethods.data.length > 0) { const pm = paymentMethods.data[0]; // Just take the first one return res.json({ hasPaymentMethod: true, brand: pm.card.brand, last4: pm.card.last4, exp_month: pm.card.exp_month, exp_year: pm.card.exp_year }); } res.json({ hasPaymentMethod: false }); } catch (err) { console.error("Get billing info error:", err); res.status(500).json({ error: "Internal Server Error" }); } } /* ----------------------------------------------------- */ module.exports = { createCheckoutSession, createPaymentIntent, handleWebhook, cancelSubscription, getPaymentDetails, activateTrial, getUserSubscriptionStatus, createPortalSession, getBillingInfo };