feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
208
pledge-now-pay-later/prisma/schema.prisma
Normal file
208
pledge-now-pay-later/prisma/schema.prisma
Normal file
@@ -0,0 +1,208 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
country String @default("UK")
|
||||
timezone String @default("Europe/London")
|
||||
bankName String?
|
||||
bankSortCode String?
|
||||
bankAccountNo String?
|
||||
bankAccountName String?
|
||||
refPrefix String @default("PNPL")
|
||||
logo String?
|
||||
primaryColor String @default("#1e40af")
|
||||
gcAccessToken String?
|
||||
gcEnvironment String @default("sandbox")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users User[]
|
||||
events Event[]
|
||||
pledges Pledge[]
|
||||
imports Import[]
|
||||
|
||||
@@index([slug])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
hashedPassword String?
|
||||
role String @default("staff") // super_admin, org_admin, staff, volunteer
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String
|
||||
description String?
|
||||
eventDate DateTime?
|
||||
location String?
|
||||
goalAmount Int? // in pence
|
||||
currency String @default("GBP")
|
||||
status String @default("active") // draft, active, closed, archived
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
qrSources QrSource[]
|
||||
pledges Pledge[]
|
||||
|
||||
@@unique([organizationId, slug])
|
||||
@@index([organizationId, status])
|
||||
}
|
||||
|
||||
model QrSource {
|
||||
id String @id @default(cuid())
|
||||
label String // "Table 5", "Volunteer: Ahmed"
|
||||
code String @unique // short token for URL
|
||||
volunteerName String?
|
||||
tableName String?
|
||||
eventId String
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
scanCount Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
pledges Pledge[]
|
||||
|
||||
@@index([eventId])
|
||||
@@index([code])
|
||||
}
|
||||
|
||||
model Pledge {
|
||||
id String @id @default(cuid())
|
||||
reference String @unique // human-safe bank ref e.g. "PNPL-7K4P-50"
|
||||
amountPence Int
|
||||
currency String @default("GBP")
|
||||
rail String // bank, gocardless, card
|
||||
status String @default("new") // new, initiated, paid, overdue, cancelled
|
||||
donorName String?
|
||||
donorEmail String?
|
||||
donorPhone String?
|
||||
giftAid Boolean @default(false)
|
||||
iPaidClickedAt DateTime?
|
||||
notes String?
|
||||
|
||||
eventId String
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
qrSourceId String?
|
||||
qrSource QrSource? @relation(fields: [qrSourceId], references: [id])
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
|
||||
paymentInstruction PaymentInstruction?
|
||||
payments Payment[]
|
||||
reminders Reminder[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
paidAt DateTime?
|
||||
cancelledAt DateTime?
|
||||
|
||||
@@index([organizationId, status])
|
||||
@@index([reference])
|
||||
@@index([eventId, status])
|
||||
@@index([donorEmail])
|
||||
@@index([donorPhone])
|
||||
}
|
||||
|
||||
model PaymentInstruction {
|
||||
id String @id @default(cuid())
|
||||
pledgeId String @unique
|
||||
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
|
||||
bankReference String // the unique ref to use
|
||||
bankDetails Json // {sortCode, accountNo, accountName, bankName}
|
||||
gcMandateId String?
|
||||
gcMandateUrl String?
|
||||
sentAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([bankReference])
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
pledgeId String
|
||||
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
|
||||
provider String // bank, gocardless, stripe
|
||||
providerRef String? // external ID
|
||||
amountPence Int
|
||||
status String @default("pending") // pending, confirmed, failed
|
||||
matchedBy String? // auto, manual
|
||||
receivedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
importId String?
|
||||
import Import? @relation(fields: [importId], references: [id])
|
||||
|
||||
@@index([pledgeId])
|
||||
@@index([providerRef])
|
||||
}
|
||||
|
||||
model Reminder {
|
||||
id String @id @default(cuid())
|
||||
pledgeId String
|
||||
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
|
||||
step Int // 0=instructions, 1=nudge, 2=urgency, 3=final
|
||||
channel String @default("email") // email, sms, whatsapp
|
||||
scheduledAt DateTime
|
||||
sentAt DateTime?
|
||||
status String @default("pending") // pending, sent, skipped, failed
|
||||
payload Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([pledgeId])
|
||||
@@index([scheduledAt, status])
|
||||
}
|
||||
|
||||
model Import {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
kind String // bank_statement, gocardless_export, crm_export
|
||||
fileName String?
|
||||
rowCount Int @default(0)
|
||||
matchedCount Int @default(0)
|
||||
unmatchedCount Int @default(0)
|
||||
mappingConfig Json?
|
||||
stats Json?
|
||||
status String @default("pending") // pending, processing, completed, failed
|
||||
uploadedAt DateTime @default(now())
|
||||
|
||||
payments Payment[]
|
||||
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model AnalyticsEvent {
|
||||
id String @id @default(cuid())
|
||||
eventType String // pledge_start, amount_selected, rail_selected, identity_submitted, pledge_completed, instruction_copy_clicked, i_paid_clicked, payment_matched
|
||||
pledgeId String?
|
||||
eventId String?
|
||||
qrSourceId String?
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([eventType])
|
||||
@@index([pledgeId])
|
||||
@@index([eventId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
306
pledge-now-pay-later/prisma/seed.mts
Normal file
306
pledge-now-pay-later/prisma/seed.mts
Normal file
@@ -0,0 +1,306 @@
|
||||
import "dotenv/config"
|
||||
import pg from "pg"
|
||||
import { PrismaPg } from "@prisma/adapter-pg"
|
||||
import { PrismaClient } from "../src/generated/prisma/client.ts"
|
||||
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
|
||||
const adapter = new PrismaPg(pool)
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
function daysFromNow(days: number): Date {
|
||||
return new Date(Date.now() + days * 86400000)
|
||||
}
|
||||
|
||||
function daysAgo(days: number): Date {
|
||||
return new Date(Date.now() - days * 86400000)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// ── Organisation ──
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { slug: "demo-charity" },
|
||||
update: {
|
||||
bankName: "Barclays",
|
||||
bankSortCode: "20-00-00",
|
||||
bankAccountNo: "12345678",
|
||||
bankAccountName: "Charity Right",
|
||||
},
|
||||
create: {
|
||||
name: "Charity Right",
|
||||
slug: "demo-charity",
|
||||
country: "UK",
|
||||
timezone: "Europe/London",
|
||||
bankName: "Barclays",
|
||||
bankSortCode: "20-00-00",
|
||||
bankAccountNo: "12345678",
|
||||
bankAccountName: "Charity Right",
|
||||
refPrefix: "DEMO",
|
||||
primaryColor: "#1e40af",
|
||||
},
|
||||
})
|
||||
|
||||
// ── Admin user ──
|
||||
await prisma.user.upsert({
|
||||
where: { email: "admin@charityright.org" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "admin@charityright.org",
|
||||
name: "Azreen Jamal",
|
||||
role: "org_admin",
|
||||
organizationId: org.id,
|
||||
},
|
||||
})
|
||||
|
||||
// ── Events ──
|
||||
const galaEvent = await prisma.event.upsert({
|
||||
where: { organizationId_slug: { organizationId: org.id, slug: "ramadan-gala-2026" } },
|
||||
update: { name: "Ramadan Gala 2026", eventDate: daysFromNow(14), goalAmount: 5000000 },
|
||||
create: {
|
||||
name: "Ramadan Gala 2026",
|
||||
slug: "ramadan-gala-2026",
|
||||
description: "Annual fundraising gala dinner — all proceeds support orphan education in Bangladesh, Pakistan, and Syria.",
|
||||
eventDate: daysFromNow(14),
|
||||
location: "Bradford Hilton, Hall Lane, BD1 4QR",
|
||||
goalAmount: 5000000, // £50,000
|
||||
currency: "GBP",
|
||||
status: "active",
|
||||
organizationId: org.id,
|
||||
},
|
||||
})
|
||||
|
||||
const eidEvent = await prisma.event.upsert({
|
||||
where: { organizationId_slug: { organizationId: org.id, slug: "eid-community-lunch-2026" } },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Eid Community Lunch 2026",
|
||||
slug: "eid-community-lunch-2026",
|
||||
description: "Community lunch and fundraiser for local food bank programme.",
|
||||
eventDate: daysFromNow(45),
|
||||
location: "East London Mosque, Whitechapel Road, E1 1JX",
|
||||
goalAmount: 1500000, // £15,000
|
||||
currency: "GBP",
|
||||
status: "active",
|
||||
organizationId: org.id,
|
||||
},
|
||||
})
|
||||
|
||||
// ── QR Sources for Gala ──
|
||||
const qrCodes = [
|
||||
{ label: "Table 1 - Ahmed", volunteerName: "Ahmed Khan", tableName: "Table 1", code: "gala-tbl1" },
|
||||
{ label: "Table 2 - Fatima", volunteerName: "Fatima Patel", tableName: "Table 2", code: "gala-tbl2" },
|
||||
{ label: "Table 3 - Yusuf", volunteerName: "Yusuf Ali", tableName: "Table 3", code: "gala-tbl3" },
|
||||
{ label: "Table 4 - Khadijah", volunteerName: "Khadijah Begum", tableName: "Table 4", code: "gala-tbl4" },
|
||||
{ label: "Table 5 - Omar", volunteerName: "Omar Malik", tableName: "Table 5", code: "gala-tbl5" },
|
||||
{ label: "Main Entrance", volunteerName: null, tableName: null, code: "gala-entrance" },
|
||||
{ label: "Stage Banner", volunteerName: null, tableName: null, code: "gala-stage" },
|
||||
{ label: "Online Link", volunteerName: null, tableName: null, code: "gala-online" },
|
||||
]
|
||||
|
||||
const qrSourceIds: Record<string, string> = {}
|
||||
for (const qr of qrCodes) {
|
||||
const source = await prisma.qrSource.upsert({
|
||||
where: { code: qr.code },
|
||||
update: { label: qr.label, volunteerName: qr.volunteerName, scanCount: Math.floor(Math.random() * 40) + 5 },
|
||||
create: {
|
||||
label: qr.label,
|
||||
code: qr.code,
|
||||
volunteerName: qr.volunteerName,
|
||||
tableName: qr.tableName,
|
||||
eventId: galaEvent.id,
|
||||
scanCount: Math.floor(Math.random() * 40) + 5,
|
||||
},
|
||||
})
|
||||
qrSourceIds[qr.code] = source.id
|
||||
}
|
||||
|
||||
// ── QR Sources for Eid ──
|
||||
const eidQrs = [
|
||||
{ label: "Registration Desk", volunteerName: "Ibrahim Hassan", tableName: null, code: "eid-reg" },
|
||||
{ label: "Online Link", volunteerName: null, tableName: null, code: "eid-online" },
|
||||
]
|
||||
for (const qr of eidQrs) {
|
||||
await prisma.qrSource.upsert({
|
||||
where: { code: qr.code },
|
||||
update: {},
|
||||
create: {
|
||||
label: qr.label,
|
||||
code: qr.code,
|
||||
volunteerName: qr.volunteerName,
|
||||
tableName: qr.tableName,
|
||||
eventId: eidEvent.id,
|
||||
scanCount: Math.floor(Math.random() * 10) + 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Sample Pledges ──
|
||||
const samplePledges = [
|
||||
// Paid pledges
|
||||
{ name: "Sarah Khan", email: "sarah@example.com", phone: "07700900001", amount: 10000, rail: "bank", status: "paid", giftAid: true, qr: "gala-tbl1", daysAgo: 5 },
|
||||
{ name: "Ali Hassan", email: "ali.hassan@gmail.com", phone: "07700900002", amount: 25000, rail: "bank", status: "paid", giftAid: false, qr: "gala-tbl1", daysAgo: 4 },
|
||||
{ name: "Amina Begum", email: "amina.b@hotmail.com", phone: "", amount: 5000, rail: "card", status: "paid", giftAid: true, qr: "gala-tbl2", daysAgo: 3 },
|
||||
{ name: "Mohammed Raza", email: "m.raza@outlook.com", phone: "07700900004", amount: 50000, rail: "gocardless", status: "paid", giftAid: true, qr: "gala-stage", daysAgo: 6 },
|
||||
{ name: "Zainab Ahmed", email: "zainab@example.com", phone: "", amount: 10000, rail: "bank", status: "paid", giftAid: false, qr: "gala-tbl3", daysAgo: 7 },
|
||||
{ name: "Hassan Malik", email: "hassan.malik@gmail.com", phone: "07700900006", amount: 20000, rail: "card", status: "paid", giftAid: true, qr: "gala-entrance", daysAgo: 2 },
|
||||
|
||||
// Initiated (payment in progress)
|
||||
{ name: "Ruqayyah Patel", email: "ruqayyah@example.com", phone: "07700900007", amount: 15000, rail: "bank", status: "initiated", giftAid: true, qr: "gala-tbl4", daysAgo: 1 },
|
||||
{ name: "Ibrahim Shah", email: "ibrahim.shah@gmail.com", phone: "", amount: 10000, rail: "gocardless", status: "initiated", giftAid: false, qr: "gala-tbl5", daysAgo: 1 },
|
||||
|
||||
// New pledges (just created)
|
||||
{ name: "Maryam Siddiqui", email: "maryam.s@yahoo.com", phone: "07700900009", amount: 5000, rail: "bank", status: "new", giftAid: false, qr: "gala-tbl2", daysAgo: 0 },
|
||||
{ name: "Usman Chaudhry", email: "usman.c@gmail.com", phone: "", amount: 100000, rail: "bank", status: "new", giftAid: true, qr: "gala-entrance", daysAgo: 0 },
|
||||
{ name: "Aisha Rahman", email: "aisha.r@hotmail.com", phone: "07700900011", amount: 7500, rail: "card", status: "new", giftAid: true, qr: "gala-online", daysAgo: 0 },
|
||||
{ name: null, email: "anon.donor@gmail.com", phone: "", amount: 20000, rail: "bank", status: "new", giftAid: false, qr: "gala-tbl3", daysAgo: 0 },
|
||||
|
||||
// Overdue
|
||||
{ name: "Tariq Hussain", email: "tariq.h@example.com", phone: "07700900013", amount: 25000, rail: "bank", status: "overdue", giftAid: true, qr: "gala-tbl1", daysAgo: 12 },
|
||||
{ name: "Nadia Akhtar", email: "nadia.a@outlook.com", phone: "", amount: 10000, rail: "bank", status: "overdue", giftAid: false, qr: "gala-tbl5", daysAgo: 10 },
|
||||
|
||||
// Cancelled
|
||||
{ name: "Omar Farooq", email: "omar.f@gmail.com", phone: "07700900015", amount: 5000, rail: "card", status: "cancelled", giftAid: false, qr: "gala-tbl4", daysAgo: 8 },
|
||||
|
||||
// FPX pledge (Malaysian donor)
|
||||
{ name: "Ahmad bin Abdullah", email: "ahmad@example.my", phone: "+60123456789", amount: 50000, rail: "fpx", status: "paid", giftAid: false, qr: "gala-online", daysAgo: 3 },
|
||||
|
||||
// Eid event pledges
|
||||
{ name: "Hafsa Nawaz", email: "hafsa@example.com", phone: "07700900017", amount: 5000, rail: "bank", status: "new", giftAid: true, qr: null, daysAgo: 1 },
|
||||
{ name: "Bilal Iqbal", email: "bilal.i@gmail.com", phone: "", amount: 10000, rail: "gocardless", status: "paid", giftAid: false, qr: null, daysAgo: 5 },
|
||||
]
|
||||
|
||||
let pledgeIndex = 0
|
||||
for (const p of samplePledges) {
|
||||
pledgeIndex++
|
||||
const ref = `DEMO-SEED${String(pledgeIndex).padStart(2, "0")}-${Math.floor(p.amount / 100)}`
|
||||
const isEid = p.qr === null
|
||||
const eventId = isEid ? eidEvent.id : galaEvent.id
|
||||
const createdAt = daysAgo(p.daysAgo)
|
||||
const paidAt = p.status === "paid" ? daysAgo(Math.max(p.daysAgo - 1, 0)) : null
|
||||
|
||||
// Skip if reference already exists
|
||||
const existing = await prisma.pledge.findUnique({ where: { reference: ref } })
|
||||
if (existing) continue
|
||||
|
||||
const pledge = await prisma.pledge.create({
|
||||
data: {
|
||||
reference: ref,
|
||||
amountPence: p.amount,
|
||||
currency: "GBP",
|
||||
rail: p.rail,
|
||||
status: p.status,
|
||||
donorName: p.name,
|
||||
donorEmail: p.email || null,
|
||||
donorPhone: p.phone || null,
|
||||
giftAid: p.giftAid,
|
||||
eventId,
|
||||
qrSourceId: p.qr ? qrSourceIds[p.qr] || null : null,
|
||||
organizationId: org.id,
|
||||
createdAt,
|
||||
paidAt,
|
||||
cancelledAt: p.status === "cancelled" ? daysAgo(p.daysAgo - 1) : null,
|
||||
},
|
||||
})
|
||||
|
||||
// Payment instruction for bank transfers
|
||||
if (p.rail === "bank") {
|
||||
await prisma.paymentInstruction.create({
|
||||
data: {
|
||||
pledgeId: pledge.id,
|
||||
bankReference: ref,
|
||||
bankDetails: {
|
||||
bankName: "Barclays",
|
||||
sortCode: "20-00-00",
|
||||
accountNo: "12345678",
|
||||
accountName: "Charity Right",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Payment record for paid pledges
|
||||
if (p.status === "paid") {
|
||||
await prisma.payment.create({
|
||||
data: {
|
||||
pledgeId: pledge.id,
|
||||
provider: p.rail === "gocardless" ? "gocardless" : p.rail === "card" || p.rail === "fpx" ? "stripe" : "bank",
|
||||
providerRef: p.rail === "bank" ? null : `sim_${pledge.id.slice(0, 8)}`,
|
||||
amountPence: p.amount,
|
||||
status: "confirmed",
|
||||
matchedBy: p.rail === "bank" ? "auto" : "webhook",
|
||||
receivedAt: paidAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Reminders for non-paid pledges
|
||||
if (["new", "initiated", "overdue"].includes(p.status)) {
|
||||
const steps = [
|
||||
{ step: 0, delayDays: 0, key: "instructions" },
|
||||
{ step: 1, delayDays: 2, key: "gentle_nudge" },
|
||||
{ step: 2, delayDays: 7, key: "urgency_impact" },
|
||||
{ step: 3, delayDays: 14, key: "final_reminder" },
|
||||
]
|
||||
for (const s of steps) {
|
||||
const scheduledAt = new Date(createdAt.getTime() + s.delayDays * 86400000)
|
||||
const isSent = scheduledAt < new Date() && p.status !== "new"
|
||||
await prisma.reminder.create({
|
||||
data: {
|
||||
pledgeId: pledge.id,
|
||||
step: s.step,
|
||||
channel: "email",
|
||||
scheduledAt,
|
||||
status: p.status === "overdue" && s.step <= 2 ? "sent" : isSent ? "sent" : "pending",
|
||||
sentAt: isSent ? scheduledAt : null,
|
||||
payload: { templateKey: s.key },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Analytics events
|
||||
await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
eventType: "pledge_completed",
|
||||
pledgeId: pledge.id,
|
||||
eventId,
|
||||
qrSourceId: p.qr ? qrSourceIds[p.qr] || null : null,
|
||||
metadata: { amountPence: p.amount, rail: p.rail },
|
||||
createdAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Funnel analytics (scans → starts → completions) ──
|
||||
const funnelEvents = [
|
||||
...Array.from({ length: 45 }, () => ({ eventType: "pledge_start", eventId: galaEvent.id })),
|
||||
...Array.from({ length: 8 }, () => ({ eventType: "pledge_start", eventId: eidEvent.id })),
|
||||
...Array.from({ length: 12 }, () => ({ eventType: "instruction_copy_clicked", eventId: galaEvent.id })),
|
||||
...Array.from({ length: 6 }, () => ({ eventType: "i_paid_clicked", eventId: galaEvent.id })),
|
||||
]
|
||||
for (const fe of funnelEvents) {
|
||||
await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
eventType: fe.eventType,
|
||||
eventId: fe.eventId,
|
||||
createdAt: daysAgo(Math.floor(Math.random() * 7)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Count totals
|
||||
const pledgeCount = await prisma.pledge.count({ where: { organizationId: org.id } })
|
||||
const totalAmount = await prisma.pledge.aggregate({ where: { organizationId: org.id }, _sum: { amountPence: true } })
|
||||
|
||||
console.log("✅ Seed data created")
|
||||
console.log(` Org: ${org.name} (${org.slug})`)
|
||||
console.log(` Events: ${galaEvent.name}, ${eidEvent.name}`)
|
||||
console.log(` QR Codes: ${qrCodes.length + eidQrs.length}`)
|
||||
console.log(` Pledges: ${pledgeCount} (£${((totalAmount._sum.amountPence || 0) / 100).toLocaleString()})`)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
await pool.end()
|
||||
})
|
||||
Reference in New Issue
Block a user