-
-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathroute.ts
More file actions
172 lines (147 loc) · 5.46 KB
/
route.ts
File metadata and controls
172 lines (147 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { sanityWriteClient } from '@/lib/sanity-write-client'
import { bridgeSponsorLeadToSponsor } from '@/lib/sponsor/sponsor-bridge'
/**
* Stripe webhook handler for sponsor invoices.
*
* Handles:
* - invoice.paid → update sponsorLead status to 'paid', assign to next video
* - invoice.payment_failed → update sponsorLead status back to 'negotiating'
*/
/** Lazy Stripe client — only created when actually needed */
let _stripe: Stripe | null = null
function getStripeClient(): Stripe {
if (!_stripe) {
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY not set')
}
_stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
}
return _stripe
}
export async function POST(request: Request) {
try {
const body = await request.text()
const sig = request.headers.get('stripe-signature')
if (!sig) {
console.error('[SPONSOR] Missing stripe-signature header')
return NextResponse.json({ error: 'Missing signature' }, { status: 400 })
}
if (!process.env.STRIPE_WEBHOOK_SECRET) {
console.error('[SPONSOR] STRIPE_WEBHOOK_SECRET not set')
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 })
}
// Verify webhook signature
const stripe = getStripeClient()
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET)
} catch (err) {
console.error('[SPONSOR] Webhook signature verification failed:', err)
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
console.log('[SPONSOR] Stripe webhook received:', {
type: event.type,
id: event.id,
timestamp: new Date().toISOString(),
})
switch (event.type) {
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice
console.log('[SPONSOR] Invoice paid:', invoice.id)
// Find the sponsorLead by stripeInvoiceId in Sanity
const lead = await sanityWriteClient.fetch(
`*[_type == "sponsorLead" && stripeInvoiceId == $invoiceId][0]`,
{ invoiceId: invoice.id }
)
if (!lead) {
console.warn('[SPONSOR] No sponsorLead found for invoice:', invoice.id)
break
}
// Idempotency guard — skip if already paid (Stripe retries on 5xx)
if (lead.status === 'paid') {
console.log('[SPONSOR] Lead already paid, skipping (idempotent):', lead._id)
break
}
// Update status to 'paid'
await sanityWriteClient
.patch(lead._id)
.set({
status: 'paid',
stripePaymentStatus: 'paid',
})
.commit()
console.log('[SPONSOR] Updated sponsorLead to paid:', lead._id)
// Bridge: create/link sponsor doc for content attribution
try {
await bridgeSponsorLeadToSponsor(lead._id)
console.log('[SPONSOR] Sponsor bridge completed for lead:', lead._id)
} catch (bridgeError) {
// Non-fatal — don't fail the webhook if bridge fails
console.error('[SPONSOR] Sponsor bridge failed (non-fatal):', bridgeError)
}
// Find next available automatedVideo (status script_ready or later, no sponsorSlot assigned)
const availableVideo = await sanityWriteClient.fetch(
`*[_type == "automatedVideo" && status in ["script_ready", "media_ready", "ready_to_publish"] && !defined(bookedSlot)] | order(_createdAt asc) [0]{
_id,
title,
status
}`
)
if (availableVideo) {
// Assign the lead to the video via bookedSlot
await sanityWriteClient
.patch(availableVideo._id)
.set({
bookedSlot: {
_type: 'reference',
_ref: lead._id,
},
})
.commit()
console.log('[SPONSOR] Assigned lead to video:', {
leadId: lead._id,
videoId: availableVideo._id,
videoTitle: availableVideo.title,
})
} else {
console.warn('[SPONSOR] No available video found for lead:', lead._id)
}
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
console.log('[SPONSOR] Invoice payment failed:', invoice.id)
// Find the sponsorLead by stripeInvoiceId in Sanity
const lead = await sanityWriteClient.fetch(
`*[_type == "sponsorLead" && stripeInvoiceId == $invoiceId][0]`,
{ invoiceId: invoice.id }
)
if (!lead) {
console.warn('[SPONSOR] No sponsorLead found for failed invoice:', invoice.id)
break
}
// Update sponsorLead status back to 'negotiating'
await sanityWriteClient
.patch(lead._id)
.set({
status: 'negotiating',
stripePaymentStatus: 'failed',
})
.commit()
console.log('[SPONSOR] Updated sponsorLead to negotiating (payment failed):', lead._id)
break
}
default:
console.log('[SPONSOR] Unhandled webhook event type:', event.type)
}
return NextResponse.json({ received: true })
} catch (error) {
console.error('[SPONSOR] Stripe webhook error:', error)
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
)
}
}