Add Democratic fundraising platform.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "LedgerType" ADD VALUE 'DEBIT_POLL_VOTE';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PollVote" (
|
||||
"id" TEXT NOT NULL,
|
||||
"pollSlug" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"normalizedKey" TEXT NOT NULL,
|
||||
"creditsSpent" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PollVote_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PollVote_pollSlug_userId_key" ON "PollVote"("pollSlug", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PollVote_pollSlug_normalizedKey_idx" ON "PollVote"("pollSlug", "normalizedKey");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PollVote" ADD CONSTRAINT "PollVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LedgerEntry" ADD COLUMN "pollVoteId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LedgerEntry_pollVoteId_key" ON "LedgerEntry"("pollVoteId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_pollVoteId_fkey" FOREIGN KEY ("pollVoteId") REFERENCES "PollVote"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "LedgerEntry" ADD COLUMN "raffleEntryId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LedgerEntry_raffleEntryId_key" ON "LedgerEntry"("raffleEntryId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_raffleEntryId_fkey" FOREIGN KEY ("raffleEntryId") REFERENCES "RaffleEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- AlterTable: guest donations + optional donor contact (CRM / disclosure tooling)
|
||||
ALTER TABLE "Donation" DROP CONSTRAINT IF EXISTS "Donation_userId_fkey";
|
||||
|
||||
ALTER TABLE "Donation" ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "Donation" ADD COLUMN "donorEmail" TEXT;
|
||||
ALTER TABLE "Donation" ADD COLUMN "donorName" TEXT;
|
||||
|
||||
ALTER TABLE "Donation" ADD CONSTRAINT "Donation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
95
prisma/migrations/20260510220000_casino_games/migration.sql
Normal file
95
prisma/migrations/20260510220000_casino_games/migration.sql
Normal file
@@ -0,0 +1,95 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "LedgerType" ADD VALUE 'DEBIT_GAME_BET';
|
||||
ALTER TYPE "LedgerType" ADD VALUE 'CREDIT_GAME_WIN';
|
||||
ALTER TYPE "LedgerType" ADD VALUE 'CREDIT_GAME_REFUND';
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "GameType" AS ENUM ('CRASH', 'DICE', 'MINES', 'TOWER', 'SLOTS', 'BLACKJACK', 'ROULETTE', 'COIN_FLIP', 'PONG', 'PREDICTION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RoomStatus" AS ENUM ('WAITING', 'ACTIVE', 'RESOLVED', 'EXPIRED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GameSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"gameType" "GameType" NOT NULL,
|
||||
"wageredBLW" INTEGER NOT NULL,
|
||||
"payoutBLW" INTEGER NOT NULL DEFAULT 0,
|
||||
"multiplier" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"outcome" TEXT NOT NULL,
|
||||
"serverSeed" TEXT NOT NULL,
|
||||
"clientSeed" TEXT,
|
||||
"resultData" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GameSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GameSession_userId_idx" ON "GameSession"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GameSession_userId_gameType_idx" ON "GameSession"("userId", "gameType");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GameSession" ADD CONSTRAINT "GameSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GameRoom" (
|
||||
"id" TEXT NOT NULL,
|
||||
"gameType" "GameType" NOT NULL,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
"joinerId" TEXT,
|
||||
"wageBLW" INTEGER NOT NULL,
|
||||
"status" "RoomStatus" NOT NULL DEFAULT 'WAITING',
|
||||
"resultData" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "GameRoom_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GameRoom_status_idx" ON "GameRoom"("status");
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PredictionMarket" (
|
||||
"id" TEXT NOT NULL,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
"question" TEXT NOT NULL,
|
||||
"endsAt" TIMESTAMP(3) NOT NULL,
|
||||
"resolvedTo" BOOLEAN,
|
||||
"totalYes" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalNo" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PredictionMarket_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PredictionMarket_endsAt_idx" ON "PredictionMarket"("endsAt");
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PredictionBet" (
|
||||
"id" TEXT NOT NULL,
|
||||
"marketId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"side" BOOLEAN NOT NULL,
|
||||
"blwAmount" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PredictionBet_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PredictionBet_marketId_idx" ON "PredictionBet"("marketId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PredictionBet_userId_idx" ON "PredictionBet"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PredictionBet" ADD CONSTRAINT "PredictionBet_marketId_fkey" FOREIGN KEY ("marketId") REFERENCES "PredictionMarket"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PredictionBet" ADD CONSTRAINT "PredictionBet_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
33
prisma/migrations/20260510230000_mission_spend/migration.sql
Normal file
33
prisma/migrations/20260510230000_mission_spend/migration.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "LedgerType" ADD VALUE 'DEBIT_MISSION_SPEND';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MissionSpend" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"missionSlug" TEXT NOT NULL,
|
||||
"missionTitle" TEXT NOT NULL,
|
||||
"creditsSpent" INTEGER NOT NULL,
|
||||
"supporterNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MissionSpend_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "MissionSpend_userId_idx" ON "MissionSpend"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "MissionSpend_missionSlug_idx" ON "MissionSpend"("missionSlug");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MissionSpend" ADD CONSTRAINT "MissionSpend_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LedgerEntry" ADD COLUMN "missionSpendId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LedgerEntry_missionSpendId_key" ON "LedgerEntry"("missionSpendId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_missionSpendId_fkey" FOREIGN KEY ("missionSpendId") REFERENCES "MissionSpend"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Official platform priorities + nullable creator for community rows (one non-null creatorId still unique)
|
||||
|
||||
CREATE TYPE "DemocraticInitiativeOrigin" AS ENUM ('PLATFORM', 'COMMUNITY');
|
||||
|
||||
ALTER TABLE "DemocraticInitiative" DROP CONSTRAINT IF EXISTS "DemocraticInitiative_creatorId_key";
|
||||
|
||||
ALTER TABLE "DemocraticInitiative" ADD COLUMN "origin" "DemocraticInitiativeOrigin" NOT NULL DEFAULT 'COMMUNITY';
|
||||
ALTER TABLE "DemocraticInitiative" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "DemocraticInitiative" ALTER COLUMN "creatorId" DROP NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "DemocraticInitiative_creatorId_key" ON "DemocraticInitiative"("creatorId");
|
||||
|
||||
CREATE INDEX "DemocraticInitiative_origin_sortOrder_idx" ON "DemocraticInitiative"("origin", "sortOrder");
|
||||
CREATE INDEX "DemocraticInitiative_origin_createdAt_idx" ON "DemocraticInitiative"("origin", "createdAt");
|
||||
26
prisma/migrations/20260515210000_user_username/migration.sql
Normal file
26
prisma/migrations/20260515210000_user_username/migration.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Add optional username column, backfill, then constrain.
|
||||
ALTER TABLE "User" ADD COLUMN "username" TEXT;
|
||||
|
||||
UPDATE "User"
|
||||
SET "username" =
|
||||
LEFT(
|
||||
COALESCE(
|
||||
NULLIF(
|
||||
regexp_replace(
|
||||
lower(trim(split_part("email", '@', 1))),
|
||||
'[^a-z0-9_]',
|
||||
'_',
|
||||
'g'
|
||||
),
|
||||
''
|
||||
),
|
||||
'supporter'
|
||||
),
|
||||
23
|
||||
)
|
||||
|| '_'
|
||||
|| right(replace("id", '-', ''), 8);
|
||||
|
||||
ALTER TABLE "User" ALTER COLUMN "username" SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User" ("username");
|
||||
@@ -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_] (3–32). 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 1–3 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])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "dotenv/config";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { DemocraticInitiativeOrigin, PrismaClient } from "@prisma/client";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
@@ -20,9 +20,15 @@ async function main() {
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: { passwordHash: hash, name: "Demo Supporter", role: "USER" },
|
||||
update: {
|
||||
passwordHash: hash,
|
||||
name: "Demo Supporter",
|
||||
role: "USER",
|
||||
username: "portal_demo",
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
username: "portal_demo",
|
||||
name: "Demo Supporter",
|
||||
passwordHash: hash,
|
||||
role: "USER",
|
||||
@@ -44,9 +50,11 @@ async function main() {
|
||||
passwordHash: adminHash,
|
||||
name: "Dr Jones",
|
||||
role: "ADMIN",
|
||||
username: "portal_admin",
|
||||
},
|
||||
create: {
|
||||
email: adminEmail,
|
||||
username: "portal_admin",
|
||||
name: "Dr Jones",
|
||||
passwordHash: adminHash,
|
||||
role: "ADMIN",
|
||||
@@ -95,6 +103,65 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const officialPriorities: Array<{ slug: string; sortOrder: number; title: string; description: string }> = [
|
||||
{
|
||||
slug: "priority-voting-access",
|
||||
sortOrder: 0,
|
||||
title: "Voting access & fair elections",
|
||||
description:
|
||||
"Protect early voting, drop boxes where allowed, and nonpartisan election administration — signal BWT here if this is the lane you want organizers and messaging to emphasize first.",
|
||||
},
|
||||
{
|
||||
slug: "priority-climate-jobs",
|
||||
sortOrder: 1,
|
||||
title: "Climate jobs & clean infrastructure",
|
||||
description:
|
||||
"Invest in union-scale clean energy, grid resilience, and communities transitioning off volatile fossil cycles — pledge BWT to raise the salience of green industrial policy in the program.",
|
||||
},
|
||||
{
|
||||
slug: "priority-health-affordability",
|
||||
sortOrder: 2,
|
||||
title: "Healthcare affordability",
|
||||
description:
|
||||
"Expand coverage choices, cap out-of-pocket shocks for families, and defend access to care — use BWT totals here to show democratic demand for health-centered field work.",
|
||||
},
|
||||
{
|
||||
slug: "priority-workers-rights",
|
||||
sortOrder: 3,
|
||||
title: "Workers’ rights & wages",
|
||||
description:
|
||||
"Stand with organizing, overtime fairness, and safety nets that reward work — pledges aggregate as a live scoreboard for how loud supporters want labor-forward priorities.",
|
||||
},
|
||||
{
|
||||
slug: "priority-community-safety",
|
||||
sortOrder: 4,
|
||||
title: "Community safety & prevention",
|
||||
description:
|
||||
"Fund violence interruption, mental health first responders, and common-sense safety policy without scapegoating — BWT here steers narrative and volunteer energy toward prevention-first democracy.",
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of officialPriorities) {
|
||||
await prisma.democraticInitiative.upsert({
|
||||
where: { slug: p.slug },
|
||||
update: {
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
origin: DemocraticInitiativeOrigin.PLATFORM,
|
||||
sortOrder: p.sortOrder,
|
||||
creatorId: null,
|
||||
},
|
||||
create: {
|
||||
slug: p.slug,
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
origin: DemocraticInitiativeOrigin.PLATFORM,
|
||||
sortOrder: p.sortOrder,
|
||||
creatorId: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Seed OK — demo:", email, "/", password);
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
Reference in New Issue
Block a user