feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later

This commit is contained in:
Azreen Jamal
2026-03-01 23:41:24 +08:00
parent ae242436c9
commit f832b913d5
99 changed files with 20949 additions and 74 deletions

View File

@@ -0,0 +1,133 @@
import { NextRequest, NextResponse } from "next/server"
import prisma from "@/lib/prisma"
import { createPledgeSchema } from "@/lib/validators"
import { generateReference } from "@/lib/reference"
import { calculateReminderSchedule } from "@/lib/reminders"
export async function POST(request: NextRequest) {
try {
const body = await request.json()
if (!prisma) {
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
}
const parsed = createPledgeSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid data", details: parsed.error.flatten() },
{ status: 400 }
)
}
const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, eventId, qrSourceId } = parsed.data
// 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 unique reference (retry on collision)
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++
}
if (attempts >= 10) {
return NextResponse.json({ error: "Could not generate unique reference" }, { status: 500 })
}
// Create pledge + payment instruction + reminder schedule in transaction
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pledge = await prisma.$transaction(async (tx: any) => {
const p = await tx.pledge.create({
data: {
reference,
amountPence,
currency: "GBP",
rail,
status: "new",
donorName: donorName || null,
donorEmail: donorEmail || null,
donorPhone: donorPhone || null,
giftAid,
eventId,
qrSourceId: qrSourceId || null,
organizationId: org.id,
},
})
// Create payment instruction for bank transfers
if (rail === "bank" && org.bankSortCode && org.bankAccountNo) {
await tx.paymentInstruction.create({
data: {
pledgeId: p.id,
bankReference: reference,
bankDetails: {
bankName: org.bankName || "",
sortCode: org.bankSortCode,
accountNo: org.bankAccountNo,
accountName: org.bankAccountName || org.name,
},
},
})
}
// Create reminder schedule
const schedule = calculateReminderSchedule(new Date())
await tx.reminder.createMany({
data: schedule.map((s) => ({
pledgeId: p.id,
step: s.step,
channel: s.channel,
scheduledAt: s.scheduledAt,
status: "pending",
payload: { templateKey: s.templateKey, subject: s.subject },
})),
})
// Track analytics
await tx.analyticsEvent.create({
data: {
eventType: "pledge_completed",
pledgeId: p.id,
eventId,
qrSourceId: qrSourceId || null,
metadata: { amountPence, rail },
},
})
return p
})
// Build response
const response: Record<string, unknown> = {
id: pledge.id,
reference: pledge.reference,
}
if (rail === "bank" && org.bankSortCode) {
response.bankDetails = {
bankName: org.bankName || "",
sortCode: org.bankSortCode,
accountNo: org.bankAccountNo || "",
accountName: org.bankAccountName || org.name,
}
}
return NextResponse.json(response, { status: 201 })
} catch (error) {
console.error("Pledge creation error:", error)
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}