119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
|
|
import { NextRequest, NextResponse } from "next/server";
|
||
|
|
import { prisma } from "@/lib/prisma";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Polar.sh webhook handler.
|
||
|
|
*
|
||
|
|
* In production, verify the webhook signature using Polar's webhook secret.
|
||
|
|
* For development, we accept a direct ?userId=&tier= fallback for testing.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export async function POST(req: NextRequest) {
|
||
|
|
try {
|
||
|
|
// Check for direct fallback query params (dev/testing)
|
||
|
|
const url = new URL(req.url);
|
||
|
|
const directUserId = url.searchParams.get("userId");
|
||
|
|
const directTier = url.searchParams.get("tier");
|
||
|
|
|
||
|
|
if (directUserId && directTier) {
|
||
|
|
return handleDirectUpgrade(directUserId, directTier);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Production path: Parse Polar webhook event
|
||
|
|
const body = await req.json();
|
||
|
|
const event = body.type || body.event;
|
||
|
|
|
||
|
|
if (!event) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Missing event type" },
|
||
|
|
{ status: 400 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle checkout.completed event
|
||
|
|
if (
|
||
|
|
event === "checkout.completed" ||
|
||
|
|
event === "checkout.updated"
|
||
|
|
) {
|
||
|
|
const checkout = body.data?.checkout || body.data;
|
||
|
|
if (!checkout) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Missing checkout data" },
|
||
|
|
{ status: 400 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const userId = checkout.metadata?.userId;
|
||
|
|
const priceId = checkout.product?.priceId ||
|
||
|
|
checkout.productPrice?.id ||
|
||
|
|
checkout.metadata?.priceId;
|
||
|
|
|
||
|
|
if (!userId) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Missing userId in metadata" },
|
||
|
|
{ status: 400 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine tier from priceId or product metadata
|
||
|
|
let tier: "premium" | "pro" = "premium";
|
||
|
|
if (priceId?.includes("pro") || checkout.metadata?.tier === "pro") {
|
||
|
|
tier = "pro";
|
||
|
|
}
|
||
|
|
|
||
|
|
return applyUpgrade(userId, tier);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Acknowledge other events silently
|
||
|
|
return NextResponse.json({ received: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Polar webhook error:", error);
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Webhook processing failed" },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleDirectUpgrade(userId: string, tier: string) {
|
||
|
|
if (tier !== "premium" && tier !== "pro") {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Invalid tier. Must be 'premium' or 'pro'." },
|
||
|
|
{ status: 400 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return applyUpgrade(userId, tier as "premium" | "pro");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function applyUpgrade(userId: string, tier: "premium" | "pro") {
|
||
|
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||
|
|
if (!user) {
|
||
|
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const trialEndsAt = new Date();
|
||
|
|
trialEndsAt.setDate(trialEndsAt.getDate() + 7);
|
||
|
|
|
||
|
|
const updateData: Record<string, unknown> = {
|
||
|
|
trialEndsAt,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (tier === "pro") {
|
||
|
|
updateData.isPro = true;
|
||
|
|
updateData.isPremium = false; // Pro supersedes Premium
|
||
|
|
} else {
|
||
|
|
updateData.isPremium = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
await prisma.user.update({
|
||
|
|
where: { id: userId },
|
||
|
|
data: updateData,
|
||
|
|
});
|
||
|
|
|
||
|
|
return NextResponse.json({
|
||
|
|
success: true,
|
||
|
|
tier,
|
||
|
|
trialEndsAt,
|
||
|
|
});
|
||
|
|
}
|