(req: NextRequest)
| 331 | } |
| 332 | |
| 333 | const webhookHandler = async (req: NextRequest): Promise<NextResponse> => { |
| 334 | let event: Stripe.Event |
| 335 | try { |
| 336 | const buf = await req.text() |
| 337 | const sig = req.headers.get('stripe-signature')! |
| 338 | |
| 339 | event = stripeServer.webhooks.constructEvent( |
| 340 | buf, |
| 341 | sig, |
| 342 | env.STRIPE_WEBHOOK_SECRET_KEY, |
| 343 | ) |
| 344 | } catch (err) { |
| 345 | const errorMessage = err instanceof Error ? err.message : String(err) |
| 346 | logger.error( |
| 347 | { error: errorMessage }, |
| 348 | 'Webhook signature verification failed', |
| 349 | ) |
| 350 | return NextResponse.json( |
| 351 | { error: { message: `Webhook Error: ${errorMessage}` } }, |
| 352 | { status: 400 }, |
| 353 | ) |
| 354 | } |
| 355 | |
| 356 | logger.info({ type: event.type }, 'Received Stripe webhook event') |
| 357 | |
| 358 | // BILLING_DISABLED: Acknowledge but ignore org-billing related events |
| 359 | // Return 200 to prevent Stripe from retrying (503 would cause retry storms) |
| 360 | if (!ORG_BILLING_ENABLED) { |
| 361 | const isOrgEvent = await isOrgBillingEvent(event) |
| 362 | if (isOrgEvent) { |
| 363 | logger.warn( |
| 364 | { type: event.type, eventId: event.id }, |
| 365 | 'BILLING_DISABLED: Ignoring org billing webhook event', |
| 366 | ) |
| 367 | return NextResponse.json({ |
| 368 | received: true, |
| 369 | ignored: 'org billing disabled', |
| 370 | }) |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | try { |
| 375 | switch (event.type) { |
| 376 | case 'customer.created': |
| 377 | break |
| 378 | case 'customer.subscription.created': |
| 379 | case 'customer.subscription.updated': { |
| 380 | const sub = event.data.object as Stripe.Subscription |
| 381 | if (sub.metadata?.organization_id) { |
| 382 | await handleOrganizationSubscriptionEvent(sub) |
| 383 | } else { |
| 384 | await handleSubscriptionUpdated({ stripeSubscription: sub, logger }) |
| 385 | } |
| 386 | break |
| 387 | } |
| 388 | case 'customer.subscription.deleted': { |
| 389 | const sub = event.data.object as Stripe.Subscription |
| 390 | if (sub.metadata?.organization_id) { |
nothing calls this directly
no test coverage detected