feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
118
pledge-now-pay-later/src/app/api/stripe/checkout/route.ts
Normal file
118
pledge-now-pay-later/src/app/api/stripe/checkout/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { createCheckoutSession } from "@/lib/stripe"
|
||||
import { generateReference } from "@/lib/reference"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { amountPence, donorName, donorEmail, donorPhone, giftAid, eventId, qrSourceId } = body
|
||||
|
||||
if (!prisma) {
|
||||
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
|
||||
}
|
||||
|
||||
// Get event + org
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id: eventId },
|
||||
include: { organization: true },
|
||||
})
|
||||
if (!event) {
|
||||
return NextResponse.json({ error: "Event not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const org = event.organization
|
||||
|
||||
// Generate reference
|
||||
let reference = ""
|
||||
let attempts = 0
|
||||
while (attempts < 10) {
|
||||
reference = generateReference(org.refPrefix || "PNPL", amountPence)
|
||||
const exists = await prisma.pledge.findUnique({ where: { reference } })
|
||||
if (!exists) break
|
||||
attempts++
|
||||
}
|
||||
|
||||
// Create pledge in DB
|
||||
const pledge = await prisma.pledge.create({
|
||||
data: {
|
||||
reference,
|
||||
amountPence,
|
||||
currency: "GBP",
|
||||
rail: "card",
|
||||
status: "new",
|
||||
donorName: donorName || null,
|
||||
donorEmail: donorEmail || null,
|
||||
donorPhone: donorPhone || null,
|
||||
giftAid: giftAid || false,
|
||||
eventId,
|
||||
qrSourceId: qrSourceId || null,
|
||||
organizationId: org.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Track analytics
|
||||
await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
eventType: "pledge_completed",
|
||||
pledgeId: pledge.id,
|
||||
eventId,
|
||||
qrSourceId: qrSourceId || null,
|
||||
metadata: { amountPence, rail: "card" },
|
||||
},
|
||||
})
|
||||
|
||||
// Try real Stripe checkout
|
||||
const baseUrl = process.env.BASE_URL || "http://localhost:3000"
|
||||
|
||||
const session = await createCheckoutSession({
|
||||
amountPence,
|
||||
currency: "GBP",
|
||||
pledgeId: pledge.id,
|
||||
reference,
|
||||
eventName: event.name,
|
||||
organizationName: org.name,
|
||||
donorEmail: donorEmail || undefined,
|
||||
successUrl: `${baseUrl}/p/success?pledge_id=${pledge.id}&rail=card&session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancelUrl: `${baseUrl}/p/success?pledge_id=${pledge.id}&rail=card&cancelled=true`,
|
||||
})
|
||||
|
||||
if (session) {
|
||||
// Save Stripe session reference
|
||||
await prisma.payment.create({
|
||||
data: {
|
||||
pledgeId: pledge.id,
|
||||
provider: "stripe",
|
||||
providerRef: session.sessionId,
|
||||
amountPence,
|
||||
status: "pending",
|
||||
matchedBy: "auto",
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.pledge.update({
|
||||
where: { id: pledge.id },
|
||||
data: { status: "initiated" },
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
mode: "live",
|
||||
pledgeId: pledge.id,
|
||||
reference,
|
||||
checkoutUrl: session.checkoutUrl,
|
||||
sessionId: session.sessionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback: no Stripe configured — return pledge for simulated flow
|
||||
return NextResponse.json({
|
||||
mode: "simulated",
|
||||
pledgeId: pledge.id,
|
||||
reference,
|
||||
id: pledge.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Stripe checkout error:", error)
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
88
pledge-now-pay-later/src/app/api/stripe/webhook/route.ts
Normal file
88
pledge-now-pay-later/src/app/api/stripe/webhook/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { constructWebhookEvent } from "@/lib/stripe"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.text()
|
||||
const signature = request.headers.get("stripe-signature") || ""
|
||||
|
||||
const event = constructWebhookEvent(body, signature)
|
||||
if (!event) {
|
||||
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const session = event.data.object as { id: string; metadata: Record<string, string>; payment_status: string }
|
||||
const pledgeId = session.metadata?.pledge_id
|
||||
|
||||
if (pledgeId && session.payment_status === "paid") {
|
||||
await prisma.pledge.update({
|
||||
where: { id: pledgeId },
|
||||
data: {
|
||||
status: "paid",
|
||||
paidAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// Update payment record
|
||||
await prisma.payment.updateMany({
|
||||
where: {
|
||||
pledgeId,
|
||||
providerRef: session.id,
|
||||
},
|
||||
data: {
|
||||
status: "confirmed",
|
||||
receivedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// Track analytics
|
||||
await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
eventType: "payment_matched",
|
||||
pledgeId,
|
||||
metadata: { provider: "stripe", sessionId: session.id },
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "payment_intent.succeeded": {
|
||||
const pi = event.data.object as { id: string; metadata: Record<string, string> }
|
||||
const pledgeId = pi.metadata?.pledge_id
|
||||
|
||||
if (pledgeId) {
|
||||
await prisma.pledge.update({
|
||||
where: { id: pledgeId },
|
||||
data: {
|
||||
status: "paid",
|
||||
paidAt: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "payment_intent.payment_failed": {
|
||||
const pi = event.data.object as { id: string; metadata: Record<string, string> }
|
||||
const pledgeId = pi.metadata?.pledge_id
|
||||
|
||||
if (pledgeId) {
|
||||
await prisma.pledge.update({
|
||||
where: { id: pledgeId },
|
||||
data: { status: "overdue" },
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true })
|
||||
} catch (error) {
|
||||
console.error("Stripe webhook error:", error)
|
||||
return NextResponse.json({ error: "Webhook handler failed" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user