Skip to content

Commit

Permalink
test
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminshafii committed Dec 4, 2024
1 parent b4fae3b commit 5298209
Show file tree
Hide file tree
Showing 13 changed files with 665 additions and 395 deletions.
40 changes: 25 additions & 15 deletions web/app/api/webhook/handlers/checkout-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,50 @@ import Stripe from "stripe";
function createCustomerDataFromSession(
session: Stripe.Checkout.Session
): CustomerData {
const { type = "subscription", plan = "monthly" } = session.metadata || {};

return {
userId: session.metadata?.userId,
customerId: session.customer?.toString(),
status: session.status,
paymentStatus: session.payment_status,
billingCycle: session.mode === "subscription" ? "monthly" : "lifetime",
product: session.metadata?.product_key || "default",
plan: session.metadata?.price_key || "default",
billingCycle: type === "lifetime" ? "lifetime" : plan as "monthly" | "yearly",
product: type,
plan: plan,
lastPayment: new Date(),
createdAt: new Date(session.created * 1000),
};
}

// focused on updating non-critical data like sending emails and tracking events
// most of the decisions are made either in payment intent , invoice-paid, subscription-updated.
export const handleCheckoutComplete = createWebhookHandler(
async (event) => {
const session = event.data.object as Stripe.Checkout.Session;

// Validate required metadata
if (!session.metadata?.userId) {
throw new Error("Missing required userId in metadata");
}

const customerData = createCustomerDataFromSession(session);
await updateClerkMetadata(customerData);
await trackLoopsEvent({
email: session.customer_details?.email || "",
firstName: session.customer_details?.name?.split(" ")[0],
lastName: session.customer_details?.name?.split(" ").slice(1).join(" "),
userId: customerData.userId,
eventName: "checkout_completed",
});

if (session.customer_details?.email) {
await trackLoopsEvent({
email: session.customer_details.email,
firstName: session.customer_details?.name?.split(" ")[0],
lastName: session.customer_details?.name?.split(" ").slice(1).join(" "),
userId: customerData.userId,
eventName: "checkout_completed",
data: {
type: session.metadata?.type,
plan: session.metadata?.plan,
}
});
}

return {
success: true,
message: `Successfully processed checkout for ${customerData.userId}`,
};
},
{
requiredMetadata: ["userId", "product_key", "price_key"],
}
);
43 changes: 43 additions & 0 deletions web/app/api/webhook/handlers/invoice-payment-failed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createWebhookHandler } from "../handler-factory";
import { updateClerkMetadata } from "@/lib/services/clerk";
import { trackLoopsEvent } from "@/lib/services/loops";
import Stripe from "stripe";

export const handleInvoicePaymentFailed = createWebhookHandler(
async (event) => {
const invoice = event.data.object as Stripe.Invoice;
const userId = invoice.metadata?.userId;

if (!userId) {
console.warn("No userId found in invoice metadata");
return { success: true, message: "Skipped invoice without userId" };
}

await updateClerkMetadata({
userId,
customerId: invoice.customer?.toString() || "",
status: "payment_failed",
paymentStatus: invoice.status,
product: "subscription",
plan: "none",
lastPayment: new Date(),
});

if (invoice.customer_email) {
await trackLoopsEvent({
email: invoice.customer_email,
userId,
eventName: "invoice_payment_failed",
data: {
amount: invoice.amount_due,
status: invoice.status,
},
});
}

return {
success: true,
message: `Successfully processed failed payment for ${userId}`,
};
}
);
32 changes: 17 additions & 15 deletions web/app/api/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { handleSubscriptionCanceled } from "./handlers/subscription-canceled";
import { handleCheckoutComplete } from "./handlers/checkout-complete";
import { handleInvoicePaid } from "./handlers/invoice-paid";
import { handlePaymentIntentSucceeded } from "./handlers/payment-intent-succeeded";
import { handleInvoicePaymentFailed } from "./handlers/invoice-payment-failed";
import { validateWebhookMetadata } from "@/srm.config";
import { WebhookEvent } from "./types";

const HANDLERS = {
"checkout.session.completed": handleCheckoutComplete,
"customer.subscription.deleted": handleSubscriptionCanceled,
"customer.subscription.updated": handleSubscriptionUpdated,
"invoice.paid": handleInvoicePaid,
"invoice.payment_failed": handleInvoicePaymentFailed,
"payment_intent.succeeded": handlePaymentIntentSucceeded,
"customer.subscription.updated": handleSubscriptionUpdated,
} as const;

export async function POST(req: NextRequest) {
Expand All @@ -21,43 +25,41 @@ export async function POST(req: NextRequest) {
const event = await verifyStripeWebhook(req);
const handler = HANDLERS[event.type as keyof typeof HANDLERS];

if (!handler) {
console.log({
message: `Unhandled webhook event type: ${event.type}`,
eventId: event.data.object.id,
});
// Use the validateWebhookMetadata helper from srm.config
const metadata = event.data.object.metadata;
if (metadata && !validateWebhookMetadata(metadata)) {
console.warn(`Invalid metadata for event ${event.type}`);
// Continue processing as some events may not need complete metadata
}

return NextResponse.json({
status: 200,
message: `Unhandled event type: ${event.type}`,
});
if (!handler) {
console.log(`Unhandled webhook event type: ${event.type}`);
return NextResponse.json({ message: `Unhandled event type: ${event.type}` }, { status: 200 });
}

const result = await handler(event);

if (!result.success) {
console.error({
message: "Webhook processing failed",
message: `Webhook ${event.type} processing failed`,
error: result.error,
eventId: event.data.object.id,
eventId: event.id,
duration: Date.now() - startTime,
});

return NextResponse.json({ error: result.message }, { status: 400 });
}

return NextResponse.json({
status: 200,
message: result.message,
duration: Date.now() - startTime,
});

} catch (error) {
console.error({
message: "Webhook processing error",
error,
duration: Date.now() - startTime,
});

return NextResponse.json({ error: error.message }, { status: 400 });
}
}
11 changes: 10 additions & 1 deletion web/app/api/webhook/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import Stripe from "stripe";

export type WebhookEvent = {
id: string;
type: string;
data: {
object: any;
object: Stripe.Event.Data.Object & {
metadata?: {
userId?: string;
type?: string;
plan?: string;
};
};
};
};

Expand Down
Loading

0 comments on commit 5298209

Please sign in to comment.