From 9f3ce2a50b214edfc166cc119aa7917cff389f33 Mon Sep 17 00:00:00 2001 From: Josh Illichmann Date: Sun, 27 Jul 2025 23:23:53 +1000 Subject: [PATCH 1/3] feat: Add order.paid webhook handler for subscription creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle order.paid events that contain subscription data - Reuse existing subscription creation logic for consistency - Fixes 401 errors when Polar sends order.paid instead of subscription.created 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/webhooks/polar/route.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/app/api/webhooks/polar/route.ts b/src/app/api/webhooks/polar/route.ts index 6739bb1..f58850c 100644 --- a/src/app/api/webhooks/polar/route.ts +++ b/src/app/api/webhooks/polar/route.ts @@ -90,6 +90,10 @@ export async function POST(request: NextRequest) { await handlePaymentFailed(data as any); break; + case 'order.paid': + await handleOrderPaid(data as any); + break; + default: console.log('Unhandled webhook event type:', type); } @@ -305,4 +309,27 @@ async function handlePaymentSucceeded(data: PaymentEventData) { async function handlePaymentFailed(data: PaymentEventData) { // TODO: Handle failed payment, send email notification console.log('Payment failed for subscription:', data.subscription_id); +} + +async function handleOrderPaid(data: any) { + try { + console.log('🔄 Processing order.paid webhook:', data.id); + + // Check if this order has a subscription (subscription orders) + if (!data.subscription_id || !data.subscription) { + console.log('â„šī¸ Order does not contain subscription, skipping'); + return; + } + + const subscriptionData = data.subscription; + console.log('📝 Creating subscription from order.paid event:', subscriptionData.id); + + // Use the same logic as subscription.created but with subscription data from the order + await handleSubscriptionCreated(subscriptionData); + + console.log('✅ Order paid webhook processed successfully:', data.id); + } catch (error) { + console.error('❌ Error handling order paid:', error); + throw error; + } } \ No newline at end of file From 5447a37602d296d765f1690f03a53d58c0112ee0 Mon Sep 17 00:00:00 2001 From: Josh Illichmann Date: Sun, 27 Jul 2025 23:25:00 +1000 Subject: [PATCH 2/3] feat: Add comprehensive webhook handlers for all Polar events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add handlers for customer.created/updated events - Add handlers for checkout.created/updated events - Add handlers for order.created events - Add handlers for subscription.uncanceled/revoked events - Prevents 401 errors for unhandled webhook event types - Ensures all subscribed events are properly acknowledged 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/webhooks/polar/route.ts | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/app/api/webhooks/polar/route.ts b/src/app/api/webhooks/polar/route.ts index f58850c..66bd045 100644 --- a/src/app/api/webhooks/polar/route.ts +++ b/src/app/api/webhooks/polar/route.ts @@ -94,6 +94,25 @@ export async function POST(request: NextRequest) { await handleOrderPaid(data as any); break; + case 'customer.created': + case 'customer.updated': + await handleCustomerEvent(data as any, type); + break; + + case 'checkout.created': + case 'checkout.updated': + await handleCheckoutEvent(data as any, type); + break; + + case 'order.created': + await handleOrderCreated(data as any); + break; + + case 'subscription.uncanceled': + case 'subscription.revoked': + await handleSubscriptionStatusChange(data as any, type); + break; + default: console.log('Unhandled webhook event type:', type); } @@ -332,4 +351,44 @@ async function handleOrderPaid(data: any) { console.error('❌ Error handling order paid:', error); throw error; } +} + +async function handleCustomerEvent(data: any, eventType: string) { + console.log(`â„šī¸ Customer event ${eventType}:`, data.id); + // We don't need to do anything special for customer events currently + // Just log and acknowledge +} + +async function handleCheckoutEvent(data: any, eventType: string) { + console.log(`â„šī¸ Checkout event ${eventType}:`, data.id); + // We don't need to do anything special for checkout events currently + // Just log and acknowledge +} + +async function handleOrderCreated(data: any) { + console.log('â„šī¸ Order created:', data.id); + // We don't need to do anything special for order creation currently + // The important event is order.paid +} + +async function handleSubscriptionStatusChange(data: any, eventType: string) { + try { + console.log(`🔄 Processing ${eventType} webhook:`, data.id); + + const newStatus = eventType === 'subscription.uncanceled' ? 'active' : 'canceled'; + + await db + .update(subscriptions) + .set({ + status: newStatus, + canceledAt: eventType === 'subscription.revoked' ? new Date() : null, + updatedAt: new Date(), + }) + .where(eq(subscriptions.polarSubscriptionId, data.id)); + + console.log(`✅ Subscription ${eventType} processed:`, data.id); + } catch (error) { + console.error(`❌ Error handling ${eventType}:`, error); + throw error; + } } \ No newline at end of file From e8d1ce9fb1d797ac68680e405621b03a2ae532bc Mon Sep 17 00:00:00 2001 From: Josh Illichmann Date: Sun, 27 Jul 2025 23:25:32 +1000 Subject: [PATCH 3/3] feat: Add handlers for all possible Polar webhook events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle all Polar webhook event types to prevent 401 errors - Add proper subscription status handling for active/uncanceled/revoked - Add placeholder handlers for order, refund, and informational events - Ensures webhook endpoint responds to all events Polar can send 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/webhooks/polar/route.ts | 53 ++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/app/api/webhooks/polar/route.ts b/src/app/api/webhooks/polar/route.ts index 66bd045..51171c3 100644 --- a/src/app/api/webhooks/polar/route.ts +++ b/src/app/api/webhooks/polar/route.ts @@ -110,9 +110,37 @@ export async function POST(request: NextRequest) { case 'subscription.uncanceled': case 'subscription.revoked': + case 'subscription.active': await handleSubscriptionStatusChange(data as any, type); break; + case 'customer.deleted': + case 'customer.state_changed': + await handleCustomerEvent(data as any, type); + break; + + case 'order.updated': + case 'order.refunded': + await handleOrderEvent(data as any, type); + break; + + case 'refund.created': + case 'refund.updated': + await handleRefundEvent(data as any, type); + break; + + case 'product.created': + case 'product.updated': + case 'benefit.created': + case 'benefit.updated': + case 'benefit_grant.created': + case 'benefit_grant.cycled': + case 'benefit_grant.updated': + case 'benefit_grant.revoked': + case 'organization.updated': + await handleInformationalEvent(data as any, type); + break; + default: console.log('Unhandled webhook event type:', type); } @@ -375,7 +403,12 @@ async function handleSubscriptionStatusChange(data: any, eventType: string) { try { console.log(`🔄 Processing ${eventType} webhook:`, data.id); - const newStatus = eventType === 'subscription.uncanceled' ? 'active' : 'canceled'; + let newStatus = data.status || 'active'; + if (eventType === 'subscription.uncanceled' || eventType === 'subscription.active') { + newStatus = 'active'; + } else if (eventType === 'subscription.revoked') { + newStatus = 'canceled'; + } await db .update(subscriptions) @@ -391,4 +424,22 @@ async function handleSubscriptionStatusChange(data: any, eventType: string) { console.error(`❌ Error handling ${eventType}:`, error); throw error; } +} + +async function handleOrderEvent(data: any, eventType: string) { + console.log(`â„šī¸ Order event ${eventType}:`, data.id); + // We don't need to do anything special for order updates/refunds currently + // Just log and acknowledge +} + +async function handleRefundEvent(data: any, eventType: string) { + console.log(`â„šī¸ Refund event ${eventType}:`, data.id); + // TODO: In the future, we might want to handle refunds by updating subscription status + // For now, just log and acknowledge +} + +async function handleInformationalEvent(data: any, eventType: string) { + console.log(`â„šī¸ Informational event ${eventType}:`, data.id || 'N/A'); + // These are informational events that don't require action + // Just log and acknowledge } \ No newline at end of file