Scaffold and Phase 1
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ── Organization ──────────────────────────────────────────────────────────────
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
logoUrl String?
|
||||
primaryColor String?
|
||||
stripeAccountId String?
|
||||
publicUrl String?
|
||||
localHostname String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
events AuctionEvent[]
|
||||
bidders Bidder[]
|
||||
staffUsers StaffUser[]
|
||||
}
|
||||
|
||||
// ── Staff Users ───────────────────────────────────────────────────────────────
|
||||
|
||||
model StaffUser {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
email String @unique
|
||||
name String
|
||||
role String // admin | event_manager | auctioneer | spotter | checkin_staff
|
||||
passwordHash String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
auditLogs AuditLog[]
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────────
|
||||
|
||||
model AuctionEvent {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
name String
|
||||
slug String
|
||||
description String?
|
||||
venueAddress String?
|
||||
startAt DateTime
|
||||
endAt DateTime
|
||||
status String @default("draft") // draft | published | active | closed | archived
|
||||
timezone String @default("America/New_York")
|
||||
bannerImageUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
auctions Auction[]
|
||||
bidders BidderEventEnrollment[]
|
||||
invoices Invoice[]
|
||||
donations Donation[]
|
||||
paddleRaiseCampaigns PaddleRaiseCampaign[]
|
||||
auditLogs AuditLog[]
|
||||
|
||||
@@unique([organizationId, slug])
|
||||
}
|
||||
|
||||
// ── Auctions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
model Auction {
|
||||
id String @id @default(cuid())
|
||||
eventId String
|
||||
type String // live | silent
|
||||
name String
|
||||
status String @default("draft") // draft | active | paused | closed
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
event AuctionEvent @relation(fields: [eventId], references: [id])
|
||||
items AuctionItem[]
|
||||
silentWindows SilentAuctionWindow[]
|
||||
}
|
||||
|
||||
// ── Auction Items ─────────────────────────────────────────────────────────────
|
||||
|
||||
model AuctionItem {
|
||||
id String @id @default(cuid())
|
||||
auctionId String
|
||||
lotNumber String
|
||||
title String
|
||||
description String?
|
||||
donorName String?
|
||||
category String?
|
||||
fairMarketValue Decimal?
|
||||
openingBid Decimal @default(0)
|
||||
reservePrice Decimal?
|
||||
currentHighBid Decimal?
|
||||
currentHighBidderId String?
|
||||
bidIncrement Decimal @default(10)
|
||||
state String @default("preview") // preview | active | going_once | going_twice | sold | passed | closed
|
||||
pickupNotes String?
|
||||
sortOrder Int @default(0)
|
||||
silentWindowId String?
|
||||
softCloseEnabled Boolean @default(false)
|
||||
softCloseExtendMinutes Int @default(2)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
auction Auction @relation(fields: [auctionId], references: [id])
|
||||
silentWindow SilentAuctionWindow? @relation(fields: [silentWindowId], references: [id])
|
||||
currentHighBidder Bidder? @relation("CurrentHighBids", fields: [currentHighBidderId], references: [id])
|
||||
media ItemMedia[]
|
||||
bids Bid[]
|
||||
|
||||
@@unique([auctionId, lotNumber])
|
||||
}
|
||||
|
||||
model ItemMedia {
|
||||
id String @id @default(cuid())
|
||||
itemId String
|
||||
mediaType String // image | video | document | embed
|
||||
url String
|
||||
thumbnailUrl String?
|
||||
caption String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
item AuctionItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model SilentAuctionWindow {
|
||||
id String @id @default(cuid())
|
||||
auctionId String
|
||||
name String
|
||||
opensAt DateTime
|
||||
closesAt DateTime
|
||||
softCloseEnabled Boolean @default(false)
|
||||
softCloseExtendMinutes Int @default(2)
|
||||
status String @default("pending") // pending | open | closed
|
||||
|
||||
auction Auction @relation(fields: [auctionId], references: [id])
|
||||
items AuctionItem[]
|
||||
}
|
||||
|
||||
// ── Bidders ───────────────────────────────────────────────────────────────────
|
||||
|
||||
model Bidder {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
email String?
|
||||
phone String?
|
||||
firstName String
|
||||
lastName String
|
||||
paymentMethodOnFile Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
authMethods BidderAuthMethod[]
|
||||
eventEnrollments BidderEventEnrollment[]
|
||||
bids Bid[]
|
||||
currentHighBids AuctionItem[] @relation("CurrentHighBids")
|
||||
invoices Invoice[]
|
||||
donations Donation[]
|
||||
deviceSessions DeviceSession[]
|
||||
notifications Notification[]
|
||||
}
|
||||
|
||||
model BidderAuthMethod {
|
||||
id String @id @default(cuid())
|
||||
bidderId String
|
||||
type String // email_magic_link | sms_otp
|
||||
identifier String
|
||||
verifiedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([type, identifier])
|
||||
}
|
||||
|
||||
model BidderEventEnrollment {
|
||||
id String @id @default(cuid())
|
||||
bidderId String
|
||||
eventId String
|
||||
paddleNumber String?
|
||||
tableAssignment String?
|
||||
notes String?
|
||||
checkInStatus String @default("pending") // pending | checked_in
|
||||
checkInAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id])
|
||||
event AuctionEvent @relation(fields: [eventId], references: [id])
|
||||
|
||||
@@unique([bidderId, eventId])
|
||||
@@unique([eventId, paddleNumber])
|
||||
}
|
||||
|
||||
// ── Bids ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
model Bid {
|
||||
id String @id @default(cuid())
|
||||
itemId String
|
||||
bidderId String
|
||||
amount Decimal
|
||||
clientCreatedAt DateTime
|
||||
serverReceivedAt DateTime @default(now())
|
||||
originMode String // public | local_dns | local_ip | offline_queue
|
||||
syncStatus String @default("synced") // synced | pending | conflict | rejected
|
||||
deviceId String
|
||||
clientSeq Int
|
||||
isWinning Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
item AuctionItem @relation(fields: [itemId], references: [id])
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id])
|
||||
|
||||
@@index([itemId, createdAt])
|
||||
@@index([bidderId])
|
||||
}
|
||||
|
||||
// ── Paddle Raise & Donations ──────────────────────────────────────────────────
|
||||
|
||||
model PaddleRaiseCampaign {
|
||||
id String @id @default(cuid())
|
||||
eventId String
|
||||
name String
|
||||
goal Decimal?
|
||||
totalRaised Decimal @default(0)
|
||||
tiers Json @default("[]") // number[]
|
||||
isActive Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
event AuctionEvent @relation(fields: [eventId], references: [id])
|
||||
donations Donation[]
|
||||
}
|
||||
|
||||
model Donation {
|
||||
id String @id @default(cuid())
|
||||
eventId String
|
||||
bidderId String?
|
||||
campaignId String?
|
||||
amount Decimal
|
||||
anonymous Boolean @default(false)
|
||||
stripePaymentIntentId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
event AuctionEvent @relation(fields: [eventId], references: [id])
|
||||
bidder Bidder? @relation(fields: [bidderId], references: [id])
|
||||
campaign PaddleRaiseCampaign? @relation(fields: [campaignId], references: [id])
|
||||
}
|
||||
|
||||
// ── Invoices & Payments ───────────────────────────────────────────────────────
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(cuid())
|
||||
bidderId String
|
||||
eventId String
|
||||
stripeInvoiceId String?
|
||||
totalAmount Decimal @default(0)
|
||||
paidAmount Decimal @default(0)
|
||||
status String @default("draft") // draft | open | paid | partially_paid | void
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id])
|
||||
event AuctionEvent @relation(fields: [eventId], references: [id])
|
||||
payments Payment[]
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
invoiceId String
|
||||
stripePaymentIntentId String?
|
||||
amount Decimal
|
||||
currency String @default("usd")
|
||||
status String // pending | succeeded | failed | refunded
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
}
|
||||
|
||||
// ── Device Sessions ───────────────────────────────────────────────────────────
|
||||
|
||||
model DeviceSession {
|
||||
id String @id @default(cuid())
|
||||
bidderId String
|
||||
deviceId String @unique
|
||||
userAgent String?
|
||||
lastSeenAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id])
|
||||
}
|
||||
|
||||
// ── Audit Log ─────────────────────────────────────────────────────────────────
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
eventId String?
|
||||
staffUserId String?
|
||||
action String
|
||||
entityType String
|
||||
entityId String
|
||||
payload Json?
|
||||
originMode String? // mirrors bid origin when relevant
|
||||
ipAddress String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
event AuctionEvent? @relation(fields: [eventId], references: [id])
|
||||
staffUser StaffUser? @relation(fields: [staffUserId], references: [id])
|
||||
|
||||
@@index([eventId, createdAt])
|
||||
@@index([entityType, entityId])
|
||||
}
|
||||
|
||||
// ── Notifications ─────────────────────────────────────────────────────────────
|
||||
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
bidderId String
|
||||
type String // outbid | item_closed | checkout_ready | otp | receipt
|
||||
channel String // in_app | push | email | sms
|
||||
payload Json
|
||||
sentAt DateTime?
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
bidder Bidder @relation(fields: [bidderId], references: [id])
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Seed script – creates a default Organization and one demo Event.
|
||||
* Run: npm run db:seed -w packages/server
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { slug: "demo-org" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Demo Nonprofit",
|
||||
slug: "demo-org",
|
||||
primaryColor: "#2563eb",
|
||||
publicUrl: "https://bid.example.org",
|
||||
localHostname: "auction.event.lan",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Organization: ${org.name} (${org.id})`);
|
||||
|
||||
const event = await prisma.auctionEvent.upsert({
|
||||
where: { organizationId_slug: { organizationId: org.id, slug: "gala-2026" } },
|
||||
update: {},
|
||||
create: {
|
||||
organizationId: org.id,
|
||||
name: "Annual Gala 2026",
|
||||
slug: "gala-2026",
|
||||
description: "Our flagship annual fundraising gala.",
|
||||
startAt: new Date("2026-10-15T18:00:00Z"),
|
||||
endAt: new Date("2026-10-15T23:00:00Z"),
|
||||
status: "draft",
|
||||
timezone: "America/New_York",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Event: ${event.name} (${event.id})`);
|
||||
|
||||
const liveAuction = await prisma.auction.upsert({
|
||||
where: { id: "seed-live-auction" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-live-auction",
|
||||
eventId: event.id,
|
||||
type: "live",
|
||||
name: "Live Auction",
|
||||
sortOrder: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const silentAuction = await prisma.auction.upsert({
|
||||
where: { id: "seed-silent-auction" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-silent-auction",
|
||||
eventId: event.id,
|
||||
type: "silent",
|
||||
name: "Silent Auction",
|
||||
sortOrder: 1,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Auctions: ${liveAuction.name}, ${silentAuction.name}`);
|
||||
|
||||
const admin = await prisma.staffUser.upsert({
|
||||
where: { email: "admin@example.org" },
|
||||
update: {},
|
||||
create: {
|
||||
organizationId: org.id,
|
||||
email: "admin@example.org",
|
||||
name: "Demo Admin",
|
||||
role: "admin",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Staff: ${admin.email}`);
|
||||
console.log("Seed complete.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user