Dev_Socialbuddy_Backend/src/controllers/payment.controller.js
2026-02-21 19:07:05 +00:00

466 lines
14 KiB
JavaScript

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
};