466 lines
14 KiB
JavaScript
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
|
|
};
|