Files
democratic-money/prisma/schema.prisma
root 7eac42e820 Legal pages, Threads OAuth, live wallet pill, PvP money flow + resume
- Legal: /privacy-policy and /datadeletion (shared LegalPageLayout, footer
  links, sitemap + integration tests, NEXT_PUBLIC_LEGAL_CONTACT_EMAIL).
- Threads OAuth: server-side /api/threads-exchange and /threads-callback
  page (Suspense + client component, noindex, no leaked secrets).
- Username + live wallet pill in top nav. New NavWalletBalance component
  polls /api/wallet, refreshes on tab focus, and listens to the
  `wallet:refresh` event bus so cash-outs and refunds update the nav in
  real time. Flash animation on balance changes.
- useLiveWalletBalance hook now broadcasts `wallet:refresh` after every
  fetch so games, exchange panel, wallet actions, and nav all stay in
  sync without extra polling.
- PvP fund-locking (`POST /api/games/rooms`): creator funds debited
  atomically with room creation; ledger entry tagged with `gameRoomId`.
  Joiner debit happens at join. Old double-debit of the creator is gone.
- DELETE /api/games/rooms?id=... lets a creator cancel a WAITING room
  and get an idempotent refund. Coin Flip + Pong waiting screens show a
  Cancel & Refund button.
- Pong/Coin Flip recovery: expiry sweep + boot-time `recoverOrphaned
  RoomsOnBoot()` (runs before listen()) refund both parties for any
  ACTIVE/expired rooms so a server restart never strands locked credits.
- Schema migration `20260520000000_game_ledger_links` adds optional
  `gameSessionId` + `gameRoomId` FKs to LedgerEntry (with indexes) and
  extra indexes on GameSession/GameRoom for resume + sweep queries.
- GET /api/games/active returns a user's active solo session + open
  rooms (sanitized — no mine/bomb positions). Mines and Tower clients
  rehydrate on mount so a refresh mid-round resumes instead of dropping.
- ActiveGamesBanner surfaces unfinished rounds on /wallet and /casino
  with Resume / Rejoin / Cancel & refund actions.
- ExchangePanel unified with useLiveWalletBalance; per-game header gets
  an "Open wallet →" chip; Dice clears stale result on roll; Blackjack
  reveals full dealer hand on natural blackjack/push; Mines refund
  label fixed; Tower final multiplier fixed; GameHistory error path;
  Prediction "Resolved" tab.
- Site audit + redmeFIXES triage notes (REDME-FIXSES-TRIAGE.txt,
  SITE-AUDIT.txt).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 06:43:26 +00:00

530 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
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 {
USER
ADMIN
}
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?
passwordHash String?
role UserRole @default(USER)
accounts Account[]
sessions Session[]
wallet Wallet?
ledgerEntries LedgerEntry[]
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
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Wallet {
id String @id @default(cuid())
userId String @unique
balanceCredits Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
updatedAt DateTime @updatedAt
}
model LedgerEntry {
id String @id @default(cuid())
userId String
delta Int
type LedgerType
memo String?
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])
/// Optional links for casino/PvP reconciliation — not @unique because a single
/// session or room produces multiple ledger lines (debit + payout/refund).
gameSessionId String?
gameSession GameSession? @relation(fields: [gameSessionId], references: [id], onDelete: SetNull)
gameRoomId String?
gameRoom GameRoom? @relation(fields: [gameRoomId], references: [id], onDelete: SetNull)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([gameSessionId])
@@index([gameRoomId])
}
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
/// 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)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@index([userId])
}
model PrizeSku {
id String @id @default(cuid())
slug String @unique
title String
description String @db.Text
costCredits Int
stockHint String?
redemptions Redemption[]
}
model Redemption {
id String @id @default(cuid())
userId String
prizeSkuId String
creditsSpent Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
prizeSku PrizeSku @relation(fields: [prizeSkuId], references: [id], onDelete: Cascade)
ledgerEntry LedgerEntry?
createdAt DateTime @default(now())
@@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)
ledgerEntries LedgerEntry[]
@@index([userId])
@@index([userId, gameType])
@@index([userId, gameType, outcome])
}
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
ledgerEntries LedgerEntry[]
@@index([status])
@@index([status, expiresAt])
@@index([creatorId, 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
title String
description String @db.Text
ticketCostCredits Int
endsAt DateTime?
entries RaffleEntry[]
}
model RaffleEntry {
id String @id @default(cuid())
raffleId String
userId String
tickets Int @default(1)
creditsSpent Int
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])
}