Add Democratic fundraising platform.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-05-16 00:47:44 +00:00
parent 89f9b8dc83
commit 391d90a754
141 changed files with 14989 additions and 914 deletions

View File

@@ -10,7 +10,39 @@ enum LedgerType {
CREDIT_DONATION
DEBIT_SPEND
DEBIT_RAFFLE
DEBIT_POLL_VOTE
DEBIT_MISSION_SPEND
DEBIT_INITIATIVE_SPEND
ADJUSTMENT
DEBIT_GAME_BET
CREDIT_GAME_WIN
CREDIT_GAME_REFUND
DEBIT_BILLBOARD
DEBIT_SPOTLIGHT
DEBIT_CARD_MINT
DEBIT_FAQ_SUBMIT
DEBIT_FAQ_VOTE
DEBIT_BOOST
}
enum GameType {
CRASH
DICE
MINES
TOWER
SLOTS
BLACKJACK
ROULETTE
COIN_FLIP
PONG
PREDICTION
}
enum RoomStatus {
WAITING
ACTIVE
RESOLVED
EXPIRED
}
enum UserRole {
@@ -21,6 +53,8 @@ enum UserRole {
model User {
id String @id @default(cuid())
email String @unique
/// Lowercase [a-z0-9_] (332). Used along with email for credentials login.
username String @unique
emailVerified DateTime?
name String?
image String?
@@ -35,6 +69,18 @@ model User {
donations Donation[]
raffleEntries RaffleEntry[]
redemptions Redemption[]
pollVotes PollVote[]
missionSpends MissionSpend[]
initiativeSpends InitiativeSpend[]
createdInitiatives DemocraticInitiative[]
gameSessions GameSession[]
predictionBets PredictionBet[]
billboardMessages BillboardMessage[]
spotlightBids SpotlightBid[]
supporterCards SupporterCard[]
faqSubmissions FaqSubmission[]
faqVotes FaqVote[]
movementBoosts MovementBoost[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -93,28 +139,131 @@ model LedgerEntry {
type LedgerType
memo String?
donationId String? @unique
donation Donation? @relation(fields: [donationId], references: [id])
redemptionId String? @unique
redemption Redemption? @relation(fields: [redemptionId], references: [id])
donationId String? @unique
donation Donation? @relation(fields: [donationId], references: [id])
redemptionId String? @unique
redemption Redemption? @relation(fields: [redemptionId], references: [id])
raffleEntryId String? @unique
raffleEntry RaffleEntry? @relation(fields: [raffleEntryId], references: [id])
pollVoteId String? @unique
pollVote PollVote? @relation(fields: [pollVoteId], references: [id])
missionSpendId String? @unique
missionSpend MissionSpend? @relation(fields: [missionSpendId], references: [id])
initiativeSpendId String? @unique
initiativeSpend InitiativeSpend? @relation(fields: [initiativeSpendId], references: [id])
createdAt DateTime @default(now())
billboardMessageId String? @unique
billboardMessage BillboardMessage? @relation(fields: [billboardMessageId], references: [id])
spotlightBidId String? @unique
spotlightBid SpotlightBid? @relation(fields: [spotlightBidId], references: [id])
supporterCardId String? @unique
supporterCard SupporterCard? @relation(fields: [supporterCardId], references: [id])
faqSubmissionId String? @unique
faqSubmission FaqSubmission? @relation(fields: [faqSubmissionId], references: [id])
faqVoteId String? @unique
faqVote FaqVote? @relation(fields: [faqVoteId], references: [id])
movementBoostId String? @unique
movementBoost MovementBoost? @relation(fields: [movementBoostId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model PollVote {
id String @id @default(cuid())
pollSlug String
userId String
displayName String
normalizedKey String
creditsSpent Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@unique([pollSlug, userId])
@@index([pollSlug, normalizedKey])
}
model MissionSpend {
id String @id @default(cuid())
userId String
missionSlug String
missionTitle String
creditsSpent Int
supporterNote String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([userId])
@@index([missionSlug])
}
enum DemocraticInitiativeOrigin {
PLATFORM
COMMUNITY
}
model DemocraticInitiative {
id String @id @default(cuid())
slug String @unique
/// Null for official platform priorities; set for community-authored initiatives.
creatorId String?
origin DemocraticInitiativeOrigin @default(COMMUNITY)
/// Lower sorts first among PLATFORM rows; ignored for COMMUNITY (use createdAt).
sortOrder Int @default(0)
title String
description String @db.Text
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
spends InitiativeSpend[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([origin, sortOrder])
@@index([origin, createdAt])
@@index([createdAt])
}
model InitiativeSpend {
id String @id @default(cuid())
initiativeId String
userId String
creditsSpent Int
supporterNote String?
initiative DemocraticInitiative @relation(fields: [initiativeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([initiativeId])
@@index([userId])
}
model Donation {
id String @id @default(cuid())
stripePaymentIntentId String @unique
userId String
/// Null when guest checkout (still counts toward public totals).
userId String?
amountUsdCents Int
creditsAwarded Int
currency String @default("usd")
status String @default("succeeded")
/// Optional CRM fields from guest flow (never required to donate).
donorEmail String?
donorName String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@ -148,6 +297,69 @@ model Redemption {
@@index([userId])
}
model GameSession {
id String @id @default(cuid())
userId String
gameType GameType
wageredBLW Int
payoutBLW Int @default(0)
multiplier Float @default(0)
outcome String
serverSeed String
clientSeed String?
resultData Json?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([userId, gameType])
}
model GameRoom {
id String @id @default(cuid())
gameType GameType
creatorId String
joinerId String?
wageBLW Int
status RoomStatus @default(WAITING)
resultData Json?
createdAt DateTime @default(now())
expiresAt DateTime
@@index([status])
}
model PredictionMarket {
id String @id @default(cuid())
creatorId String
question String
endsAt DateTime
resolvedTo Boolean?
totalYes Int @default(0)
totalNo Int @default(0)
createdAt DateTime @default(now())
bets PredictionBet[]
@@index([endsAt])
}
model PredictionBet {
id String @id @default(cuid())
marketId String
userId String
side Boolean
blwAmount Int
createdAt DateTime @default(now())
market PredictionMarket @relation(fields: [marketId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([marketId])
@@index([userId])
}
model Raffle {
id String @id @default(cuid())
slug String @unique
@@ -166,11 +378,137 @@ model RaffleEntry {
tickets Int @default(1)
creditsSpent Int
raffle Raffle @relation(fields: [raffleId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
raffle Raffle @relation(fields: [raffleId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([raffleId])
@@index([userId])
}
// ── Feature 1: Democracy Billboard ──────────────────────────────────────────
// Users spend BWT to post a short rally-cry message on the live public ticker.
// creditsSpent determines display weight (higher = longer / more prominent).
model BillboardMessage {
id String @id @default(cuid())
userId String
displayName String
message String @db.VarChar(140)
creditsSpent Int
expiresAt DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([expiresAt])
@@index([userId])
}
// ── Feature 2: Issue Spotlight Auction ──────────────────────────────────────
// Weekly auction: users bid BWT on a policy issue; most-funded issue becomes
// the "Issue of the Week" featured on the homepage.
model SpotlightBid {
id String @id @default(cuid())
userId String
issueSlug String
issueTitle String
creditsSpent Int
weekOf String // ISO date string of Monday: "2026-05-11"
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([weekOf, issueSlug])
@@index([userId])
}
// ── Feature 3: Supporter Trading Cards ──────────────────────────────────────
// Users mint a collectible stat card (snapshot of their donor profile).
// Tier 13 unlocked by cumulative credits ever earned.
model SupporterCard {
id String @id @default(cuid())
userId String
tier Int @default(1) // 1 = Standard, 2 = Rare, 3 = Legendary
serialNumber Int // sequential per user
creditsSpent Int
statsSnapshot Json // { totalDonatedUsd, creditsEarned, rank, etc. }
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([userId])
@@index([tier])
}
// ── Feature 4: Community FAQ Board ──────────────────────────────────────────
// Spend BWT to submit a question; spend 1 BWT to upvote others.
// Admin can approve/reject — approved questions appear in the live FAQ.
enum FaqStatus {
PENDING
APPROVED
REJECTED
}
model FaqSubmission {
id String @id @default(cuid())
userId String
displayName String
question String @db.VarChar(280)
answer String? @db.Text // filled by admin on approval
status FaqStatus @default(PENDING)
creditsSpent Int
voteTotal Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
votes FaqVote[]
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([status])
@@index([userId])
}
model FaqVote {
id String @id @default(cuid())
submissionId String
userId String
creditsSpent Int @default(1)
submission FaqSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@unique([submissionId, userId])
@@index([submissionId])
@@index([userId])
}
// ── Feature 5: Power the Movement Meter ─────────────────────────────────────
// A community energy meter. Any user can add BWT to charge it up.
// When the meter hits the target, a milestone event fires and all contributors
// get a BWT bonus proportional to their contribution.
model MovementBoost {
id String @id @default(cuid())
userId String
creditsSpent Int
epochId Int @default(1) // increments each time the meter resets
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([epochId])
@@index([userId])
}