mirror of
https://github.com/vas3k/TaxHacker.git
synced 2026-01-06 05:30:08 -06:00
feat: stripe integration
This commit is contained in:
46
app/api/stripe/checkout/route.ts
Normal file
46
app/api/stripe/checkout/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import config from "@/lib/config"
|
||||
import { PLANS, stripeClient } from "@/lib/stripe"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const code = searchParams.get("code")
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json({ error: "Missing plan code" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!stripeClient) {
|
||||
return NextResponse.json({ error: "Stripe is not enabled" }, { status: 500 })
|
||||
}
|
||||
|
||||
const plan = PLANS[code]
|
||||
if (!plan || !plan.isAvailable) {
|
||||
return NextResponse.json({ error: "Invalid or inactive plan" }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await stripeClient.checkout.sessions.create({
|
||||
billing_address_collection: "auto",
|
||||
line_items: [
|
||||
{
|
||||
price: plan.stripePriceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
success_url: config.stripe.paymentSuccessUrl,
|
||||
cancel_url: config.stripe.paymentCancelUrl,
|
||||
})
|
||||
|
||||
if (!session.url) {
|
||||
console.log(session)
|
||||
return NextResponse.json({ error: `Failed to create checkout session: ${session}` }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ session })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return NextResponse.json({ error: `Failed to create checkout session: ${error}` }, { status: 500 })
|
||||
}
|
||||
}
|
||||
30
app/api/stripe/portal/route.ts
Normal file
30
app/api/stripe/portal/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getCurrentUser } from "@/lib/auth"
|
||||
import { stripeClient } from "@/lib/stripe"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const user = await getCurrentUser()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
if (!stripeClient) {
|
||||
return new NextResponse("Stripe client is not initialized", { status: 500 })
|
||||
}
|
||||
|
||||
try {
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: "No Stripe customer ID found for this user" }, { status: 400 })
|
||||
}
|
||||
|
||||
const portalSession = await stripeClient.billingPortal.sessions.create({
|
||||
customer: user.stripeCustomerId,
|
||||
return_url: `${request.nextUrl.origin}/settings/profile`,
|
||||
})
|
||||
|
||||
return NextResponse.redirect(portalSession.url)
|
||||
} catch (error) {
|
||||
console.error("Stripe portal error:", error)
|
||||
return NextResponse.json({ error: "Failed to create Stripe portal session" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
80
app/api/stripe/webhook/route.ts
Normal file
80
app/api/stripe/webhook/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import config from "@/lib/config"
|
||||
import { PLANS, stripeClient } from "@/lib/stripe"
|
||||
import { createUserDefaults, isDatabaseEmpty } from "@/models/defaults"
|
||||
import { getOrCreateCloudUser, getUserByStripeCustomerId, updateUser } from "@/models/users"
|
||||
import { NextResponse } from "next/server"
|
||||
import Stripe from "stripe"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const signature = request.headers.get("stripe-signature")
|
||||
const body = await request.text()
|
||||
|
||||
if (!signature || !config.stripe.webhookSecret) {
|
||||
return new NextResponse("Webhook signature or secret missing", { status: 400 })
|
||||
}
|
||||
|
||||
if (!stripeClient) {
|
||||
return new NextResponse("Stripe client is not initialized", { status: 500 })
|
||||
}
|
||||
|
||||
let event: Stripe.Event
|
||||
|
||||
try {
|
||||
event = stripeClient.webhooks.constructEvent(body, signature, config.stripe.webhookSecret)
|
||||
} catch (err) {
|
||||
console.error(`Webhook signature verification failed:`, err)
|
||||
return new NextResponse("Webhook signature verification failed", { status: 400 })
|
||||
}
|
||||
|
||||
console.log("Webhook event:", event)
|
||||
|
||||
// Handle the event
|
||||
try {
|
||||
switch (event.type) {
|
||||
case "customer.subscription.created":
|
||||
case "customer.subscription.updated": {
|
||||
const subscription = event.data.object as Stripe.Subscription
|
||||
const customerId = subscription.customer as string
|
||||
const item = subscription.items.data[0]
|
||||
|
||||
// Get the plan from our plans configuration
|
||||
const plan = Object.values(PLANS).find((p) => p.stripePriceId === item.price.id)
|
||||
if (!plan) {
|
||||
throw new Error(`Plan not found for price ID: ${item.price.id}`)
|
||||
}
|
||||
|
||||
let user = await getUserByStripeCustomerId(customerId)
|
||||
if (!user) {
|
||||
const customer = (await stripeClient.customers.retrieve(customerId)) as Stripe.Customer
|
||||
user = await getOrCreateCloudUser(customer.email as string, {
|
||||
email: customer.email as string,
|
||||
name: customer.name as string,
|
||||
stripeCustomerId: customer.id,
|
||||
})
|
||||
|
||||
if (await isDatabaseEmpty(user.id)) {
|
||||
await createUserDefaults(user.id)
|
||||
}
|
||||
}
|
||||
|
||||
await updateUser(user.id, {
|
||||
membershipPlan: plan.code,
|
||||
membershipExpiresAt: new Date(item.current_period_end * 1000),
|
||||
storageLimit: plan.limits.storage,
|
||||
aiBalance: plan.limits.ai,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type ${event.type}`)
|
||||
}
|
||||
|
||||
return new NextResponse("Webhook processed successfully", { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error processing webhook:", error)
|
||||
return new NextResponse("Webhook processing failed", { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user