4297 lines
155 KiB
Plaintext
4297 lines
155 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTH & USERS
|
|
// ============================================================================
|
|
|
|
enum UserRole {
|
|
SUPER_ADMIN
|
|
INFLUENCE_ADMIN
|
|
MAP_ADMIN
|
|
USER
|
|
TEMP
|
|
}
|
|
|
|
enum UserStatus {
|
|
ACTIVE
|
|
INACTIVE
|
|
SUSPENDED
|
|
EXPIRED
|
|
PENDING_VERIFICATION
|
|
PENDING_APPROVAL
|
|
}
|
|
|
|
enum UserCreatedVia {
|
|
ADMIN
|
|
PUBLIC_SHIFT_SIGNUP
|
|
STANDARD
|
|
SELF_REGISTRATION
|
|
QUICK_JOIN_INVITE
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
password String // bcrypt hashed
|
|
name String?
|
|
phone String?
|
|
role UserRole @default(USER)
|
|
roles Json @default("[]") // Array of UserRole strings for multi-role support
|
|
status UserStatus @default(ACTIVE)
|
|
permissions Json? // Per-app granular permissions
|
|
createdVia UserCreatedVia @default(STANDARD)
|
|
expiresAt DateTime? // For temp users
|
|
expireDays Int?
|
|
lastLoginAt DateTime?
|
|
emailVerified Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
refreshTokens RefreshToken[]
|
|
campaignsCreated Campaign[] @relation("CampaignCreator")
|
|
campaignsReviewed Campaign[] @relation("CampaignReviewer")
|
|
campaignEmails CampaignEmail[] @relation("CampaignEmailSender")
|
|
responses RepresentativeResponse[] @relation("ResponseSubmitter")
|
|
responseUpvotes ResponseUpvote[]
|
|
shiftSignups ShiftSignup[]
|
|
locationsCreated Location[] @relation("LocationCreator")
|
|
locationsUpdated Location[] @relation("LocationUpdater")
|
|
addressesCreated Address[] @relation("AddressCreator")
|
|
addressesUpdated Address[] @relation("AddressUpdater")
|
|
locationEdits LocationHistory[] @relation("LocationHistoryUser")
|
|
cutsCreated Cut[] @relation("CutCreator")
|
|
canvassVisits CanvassVisit[] @relation("CanvassVisitor")
|
|
canvassSessions CanvassSession[] @relation("CanvassSessions")
|
|
trackingSessions TrackingSession[] @relation("TrackingSessions")
|
|
templatesCreated EmailTemplate[] @relation("TemplatesCreated")
|
|
templatesUpdated EmailTemplate[] @relation("TemplatesUpdated")
|
|
templateVersionsCreated EmailTemplateVersion[] @relation("TemplateVersionsCreated")
|
|
templateTestsSent EmailTemplateTestLog[] @relation("TemplateTestsSent")
|
|
|
|
// Media API relations
|
|
videosUploaded Video[] @relation("VideoUploader")
|
|
videosLocked Video[] @relation("VideoLocker")
|
|
videoViews VideoView[] @relation("VideoViews")
|
|
videoScheduleHistory VideoScheduleHistory[] @relation("VideoScheduleHistory")
|
|
sessions Session[] @relation("SessionUser")
|
|
comments Comment[] @relation("CommentUser")
|
|
authTokens AuthToken[] @relation("AuthTokenUser")
|
|
sessionBansMade SessionBan[] @relation("SessionBanner")
|
|
commentModerations CommentModeration[] @relation("CommentModerator")
|
|
emailVerificationTokens EmailVerificationToken[] @relation("EmailVerificationTokens")
|
|
passwordResetTokens PasswordResetToken[] @relation("PasswordResetTokens")
|
|
emailChangeTokens EmailChangeToken[] @relation("EmailChangeTokens")
|
|
achievements UserAchievement[] @relation("UserAchievements")
|
|
stats UserStats? @relation("UserStats")
|
|
finishes UserFinish[] @relation("UserFinishes")
|
|
videoReactions VideoReaction[] @relation("VideoReactions")
|
|
highlightCooldowns HighlightCooldown[] @relation("HighlightCooldowns")
|
|
dailyActivity UserDailyActivity[] @relation("UserDailyActivity")
|
|
chatThreadReadStatus ChatThreadReadStatus[] @relation("ChatThreadReadStatus")
|
|
moderationWordLists ModerationWordList[] @relation("ModerationWordListCreator")
|
|
contentReportsSubmitted ContentReport[] @relation("ContentReportUser")
|
|
contentReportsResolved ContentReport[] @relation("ContentReportResolver")
|
|
playlists Playlist[] @relation("UserPlaylists")
|
|
featuredPlaylists FeaturedPlaylist[] @relation("FeaturedPlaylistFeaturer")
|
|
adImpressions AdImpression[] @relation("AdImpressionUser")
|
|
adClicks AdClick[] @relation("AdClickUser")
|
|
friendships Friendship[] @relation("UserFriendships")
|
|
friends Friendship[] @relation("UserFriends")
|
|
blocks UserBlock[] @relation("UserBlocks")
|
|
blockedBy UserBlock[] @relation("UserBlockedBy")
|
|
pokesSent Poke[] @relation("PokesSent")
|
|
pokesReceived Poke[] @relation("PokesReceived")
|
|
recommendationsSent VideoRecommendation[] @relation("RecommendationsSent")
|
|
recommendationsReceived VideoRecommendation[] @relation("RecommendationsReceived")
|
|
presence UserPresence? @relation("UserPresence")
|
|
galleryImages UserGalleryImage[] @relation("UserGalleryImages")
|
|
socialLinks UserSocialLink[] @relation("UserSocialLinks")
|
|
privacySettings PrivacySettings? @relation("PrivacySettings")
|
|
closeFriends CloseFriend[] @relation("CloseFriends")
|
|
closeFriendOf CloseFriend[] @relation("CloseFriendOf")
|
|
socialGroupMemberships SocialGroupMember[] @relation("SocialGroupMember")
|
|
uploads UserUpload[] @relation("UserUploads")
|
|
uploadReviews UserUpload[] @relation("UserUploadReviewer")
|
|
uploadInvites UploadInvite[] @relation("UploadInviteCreator")
|
|
tagPreferences UserTagPreference[] @relation("UserTagPreferences")
|
|
performerDiscrepancies PerformerDiscrepancy[] @relation("PerformerDiscrepancyResolver")
|
|
watchPartiesHosted WatchPartySession[] @relation("WatchPartyHost")
|
|
watchPartyParticipations WatchPartyParticipant[] @relation("WatchPartyParticipant")
|
|
watchPartyChatMessages WatchPartyChatMessage[] @relation("WatchPartyChatUser")
|
|
watchPartyReactions WatchPartyReaction[] @relation("WatchPartyReactionUser")
|
|
watchPartyInvitesSent WatchPartyInvite[] @relation("WatchPartyInviter")
|
|
watchPartyInvitesReceived WatchPartyInvite[] @relation("WatchPartyInvitee")
|
|
subscriptions UserSubscription[] @relation("UserSubscriptions")
|
|
invoices Invoice[] @relation("UserInvoices")
|
|
payments Payment[] @relation("UserPayments")
|
|
paymentAudits PaymentAuditLog[] @relation("PaymentAuditUser")
|
|
orders Order[] @relation("UserOrders")
|
|
notifications Notification[] @relation("UserNotifications")
|
|
notificationPreferences NotificationPreferences? @relation("NotificationPreferences")
|
|
|
|
// Photo gallery relations
|
|
photosUploaded Photo[] @relation("PhotoUploader")
|
|
albumsCreated PhotoAlbum[] @relation("AlbumCreator")
|
|
photoComments PhotoComment[] @relation("PhotoCommentUser")
|
|
|
|
// SMS campaign relations
|
|
smsContactListsCreated SmsContactList[] @relation("SmsContactListCreator")
|
|
smsCampaignsCreated SmsCampaign[] @relation("SmsCampaignCreator")
|
|
smsTemplatesCreated SmsMessageTemplate[] @relation("SmsTemplateCreator")
|
|
|
|
// Donation pages
|
|
donationPagesCreated DonationPage[] @relation("DonationPageCreator")
|
|
|
|
// Meetings (Jitsi)
|
|
meetingsCreated Meeting[] @relation("MeetingCreator")
|
|
|
|
// People CRM
|
|
contact Contact? @relation("UserContact")
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(cuid())
|
|
token String @unique
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@map("refresh_tokens")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — CAMPAIGNS
|
|
// ============================================================================
|
|
|
|
enum CampaignStatus {
|
|
DRAFT
|
|
ACTIVE
|
|
PAUSED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum CampaignModerationStatus {
|
|
PENDING_REVIEW
|
|
APPROVED
|
|
REJECTED
|
|
CHANGES_REQUESTED
|
|
}
|
|
|
|
enum GovernmentLevel {
|
|
FEDERAL
|
|
PROVINCIAL
|
|
MUNICIPAL
|
|
SCHOOL_BOARD
|
|
}
|
|
|
|
model Campaign {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
title String
|
|
description String? @db.Text
|
|
emailSubject String
|
|
emailBody String @db.Text
|
|
callToAction String? @db.Text
|
|
coverPhoto String?
|
|
coverVideoId Int?
|
|
status CampaignStatus @default(DRAFT)
|
|
|
|
// Feature flags
|
|
allowSmtpEmail Boolean @default(true)
|
|
allowMailtoLink Boolean @default(true)
|
|
collectUserInfo Boolean @default(true)
|
|
showEmailCount Boolean @default(true)
|
|
showCallCount Boolean @default(true)
|
|
allowEmailEditing Boolean @default(false)
|
|
allowCustomRecipients Boolean @default(false)
|
|
showResponseWall Boolean @default(false)
|
|
highlightCampaign Boolean @default(false)
|
|
|
|
// Targeting
|
|
targetGovernmentLevels GovernmentLevel[]
|
|
|
|
// Creator
|
|
createdByUserId String?
|
|
createdByUser User? @relation("CampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
createdByUserEmail String?
|
|
createdByUserName String?
|
|
|
|
// User-generated campaign moderation
|
|
isUserGenerated Boolean @default(false)
|
|
moderationStatus CampaignModerationStatus?
|
|
reviewedByUserId String?
|
|
reviewedByUser User? @relation("CampaignReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull)
|
|
reviewedAt DateTime?
|
|
rejectionReason String? @db.Text
|
|
moderationNotes String? @db.Text
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
emails CampaignEmail[]
|
|
responses RepresentativeResponse[]
|
|
customRecipients CustomRecipient[]
|
|
calls Call[]
|
|
smsCampaigns SmsCampaign[] @relation("SmsCampaigns")
|
|
|
|
@@index([moderationStatus])
|
|
@@index([isUserGenerated])
|
|
@@map("campaigns")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — REPRESENTATIVES
|
|
// ============================================================================
|
|
|
|
model Representative {
|
|
id String @id @default(cuid())
|
|
postalCode String
|
|
name String?
|
|
email String?
|
|
districtName String?
|
|
electedOffice String?
|
|
partyName String?
|
|
representativeSetName String?
|
|
url String?
|
|
photoUrl String?
|
|
offices Json? // JSON array of office contact info
|
|
cachedAt DateTime @default(now())
|
|
|
|
@@index([postalCode])
|
|
@@map("representatives")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — CAMPAIGN EMAILS
|
|
// ============================================================================
|
|
|
|
enum EmailMethod {
|
|
SMTP
|
|
MAILTO
|
|
}
|
|
|
|
enum CampaignEmailStatus {
|
|
QUEUED
|
|
SENT
|
|
FAILED
|
|
CLICKED
|
|
USER_INFO_CAPTURED
|
|
}
|
|
|
|
model CampaignEmail {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
campaignSlug String
|
|
|
|
// Sender info
|
|
userId String?
|
|
user User? @relation("CampaignEmailSender", fields: [userId], references: [id], onDelete: SetNull)
|
|
userEmail String?
|
|
userName String?
|
|
userPostalCode String?
|
|
|
|
// Recipient info
|
|
recipientEmail String
|
|
recipientName String?
|
|
recipientTitle String?
|
|
recipientLevel GovernmentLevel?
|
|
|
|
emailMethod EmailMethod
|
|
subject String
|
|
message String @db.Text
|
|
status CampaignEmailStatus @default(SENT)
|
|
senderIp String?
|
|
sentAt DateTime @default(now())
|
|
|
|
@@index([campaignId])
|
|
@@index([campaignSlug])
|
|
@@index([userPostalCode])
|
|
@@index([sentAt])
|
|
@@map("campaign_emails")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — REPRESENTATIVE RESPONSES (Response Wall)
|
|
// ============================================================================
|
|
|
|
enum ResponseType {
|
|
EMAIL
|
|
LETTER
|
|
PHONE_CALL
|
|
MEETING
|
|
SOCIAL_MEDIA
|
|
OTHER
|
|
}
|
|
|
|
enum ResponseStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model RepresentativeResponse {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
campaignSlug String
|
|
|
|
representativeName String
|
|
representativeTitle String?
|
|
representativeLevel GovernmentLevel
|
|
representativeEmail String?
|
|
|
|
responseType ResponseType
|
|
responseText String @db.Text
|
|
userComment String? @db.Text
|
|
screenshotUrl String?
|
|
|
|
// Submitter info
|
|
submittedByUserId String?
|
|
submittedByUser User? @relation("ResponseSubmitter", fields: [submittedByUserId], references: [id], onDelete: SetNull)
|
|
submittedByName String?
|
|
submittedByEmail String?
|
|
isAnonymous Boolean @default(false)
|
|
|
|
// Moderation
|
|
status ResponseStatus @default(PENDING)
|
|
|
|
// Verification
|
|
isVerified Boolean @default(false)
|
|
verificationToken String?
|
|
verificationSentAt DateTime?
|
|
verifiedAt DateTime?
|
|
verifiedBy String?
|
|
|
|
upvoteCount Int @default(0)
|
|
submittedIp String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
upvotes ResponseUpvote[]
|
|
|
|
@@index([campaignId])
|
|
@@index([campaignSlug])
|
|
@@index([representativeName])
|
|
@@map("representative_responses")
|
|
}
|
|
|
|
model ResponseUpvote {
|
|
id String @id @default(cuid())
|
|
responseId String
|
|
response RepresentativeResponse @relation(fields: [responseId], references: [id], onDelete: Cascade)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
userEmail String?
|
|
upvotedIp String?
|
|
|
|
@@unique([responseId, userId])
|
|
@@unique([responseId, upvotedIp])
|
|
@@map("response_upvotes")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — CUSTOM RECIPIENTS
|
|
// ============================================================================
|
|
|
|
model CustomRecipient {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
campaignSlug String
|
|
recipientName String
|
|
recipientEmail String
|
|
recipientTitle String?
|
|
recipientOrganization String?
|
|
notes String? @db.Text
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([campaignId])
|
|
@@map("custom_recipients")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — POSTAL CODE CACHE
|
|
// ============================================================================
|
|
|
|
model PostalCodeCache {
|
|
id String @id @default(cuid())
|
|
postalCode String @unique
|
|
city String?
|
|
province String?
|
|
centroidLat Decimal? @db.Decimal(10, 8)
|
|
centroidLng Decimal? @db.Decimal(11, 8)
|
|
lastUpdated DateTime @default(now())
|
|
|
|
@@map("postal_code_cache")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — EMAIL LOG & VERIFICATION
|
|
// ============================================================================
|
|
|
|
model EmailLog {
|
|
id String @id @default(cuid())
|
|
recipientEmail String
|
|
senderName String
|
|
senderEmail String
|
|
subject String?
|
|
message String? @db.Text
|
|
postalCode String?
|
|
status String @default("sent") // sent, failed, previewed
|
|
senderIp String?
|
|
sentAt DateTime @default(now())
|
|
|
|
@@map("email_logs")
|
|
}
|
|
|
|
model EmailVerification {
|
|
id String @id @default(cuid())
|
|
token String @unique
|
|
email String
|
|
tempCampaignData String? @db.Text // JSON
|
|
createdAt DateTime @default(now())
|
|
expiresAt DateTime
|
|
used Boolean @default(false)
|
|
|
|
@@map("email_verifications")
|
|
}
|
|
|
|
// ============================================================================
|
|
// INFLUENCE — CALL TRACKING
|
|
// ============================================================================
|
|
|
|
model Call {
|
|
id String @id @default(cuid())
|
|
representativeName String
|
|
representativeTitle String?
|
|
phoneNumber String
|
|
officeType String?
|
|
callerName String?
|
|
callerEmail String?
|
|
postalCode String?
|
|
campaignId String?
|
|
campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
|
|
campaignSlug String?
|
|
callerIp String?
|
|
calledAt DateTime @default(now())
|
|
|
|
@@index([campaignId])
|
|
@@map("calls")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — LOCATIONS
|
|
// ============================================================================
|
|
|
|
enum SupportLevel {
|
|
LEVEL_1 @map("1")
|
|
LEVEL_2 @map("2")
|
|
LEVEL_3 @map("3")
|
|
LEVEL_4 @map("4")
|
|
}
|
|
|
|
enum GeocodeProvider {
|
|
GOOGLE
|
|
MAPBOX
|
|
NOMINATIM
|
|
PHOTON
|
|
LOCATIONIQ
|
|
ARCGIS
|
|
UNKNOWN
|
|
}
|
|
|
|
enum BuildingType {
|
|
SINGLE_FAMILY
|
|
MULTI_UNIT
|
|
MIXED_USE
|
|
COMMERCIAL
|
|
}
|
|
|
|
model Location {
|
|
id String @id @default(cuid())
|
|
latitude Decimal @db.Decimal(10, 8) // Required (was nullable)
|
|
longitude Decimal @db.Decimal(11, 8) // Required (was nullable)
|
|
|
|
// Building-level data
|
|
address String // Base street address (no unit number)
|
|
postalCode String?
|
|
province String?
|
|
federalDistrict String?
|
|
buildingUse Int? // NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown
|
|
|
|
// NAR + building metadata
|
|
locGuid String? @unique
|
|
buildingType BuildingType @default(SINGLE_FAMILY)
|
|
totalUnits Int @default(1)
|
|
buildingNotes String? @db.Text // Access codes, manager contact, etc.
|
|
|
|
// Geocoding
|
|
geocodeConfidence Int? // 0-100
|
|
geocodeProvider GeocodeProvider?
|
|
|
|
// Audit
|
|
createdByUserId String?
|
|
createdByUser User? @relation("LocationCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
updatedByUserId String?
|
|
updatedByUser User? @relation("LocationUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
addresses Address[]
|
|
history LocationHistory[]
|
|
|
|
@@index([latitude, longitude])
|
|
@@index([latitude])
|
|
@@index([longitude])
|
|
@@index([postalCode])
|
|
@@map("locations")
|
|
}
|
|
|
|
model Address {
|
|
id String @id @default(cuid())
|
|
locationId String
|
|
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
|
|
|
// Unit identification
|
|
unitNumber String?
|
|
addrGuid String? @unique // NAR ADDR_GUID
|
|
|
|
// Occupant/contact info (per-unit)
|
|
firstName String?
|
|
lastName String?
|
|
email String?
|
|
phone String?
|
|
|
|
// Canvassing data (per-unit)
|
|
supportLevel SupportLevel?
|
|
sign Boolean @default(false)
|
|
signSize String?
|
|
notes String? @db.Text
|
|
|
|
// Audit
|
|
createdByUserId String?
|
|
createdByUser User? @relation("AddressCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
updatedByUserId String?
|
|
updatedByUser User? @relation("AddressUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
canvassVisits CanvassVisit[]
|
|
contactAddresses ContactAddress[]
|
|
|
|
@@index([locationId])
|
|
@@index([locationId, id])
|
|
@@index([locationId, unitNumber])
|
|
@@map("addresses")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — LOCATION HISTORY
|
|
// ============================================================================
|
|
|
|
enum LocationHistoryAction {
|
|
CREATED
|
|
UPDATED
|
|
GEOCODED
|
|
BULK_GEOCODED
|
|
MOVED_ON_MAP
|
|
IMPORTED_CSV
|
|
IMPORTED_NAR
|
|
}
|
|
|
|
model LocationHistory {
|
|
id String @id @default(cuid())
|
|
locationId String
|
|
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
|
userId String?
|
|
user User? @relation("LocationHistoryUser", fields: [userId], references: [id], onDelete: SetNull)
|
|
action LocationHistoryAction
|
|
field String? // Which field changed
|
|
oldValue String? @db.Text
|
|
newValue String? @db.Text
|
|
metadata Json? // Provider, confidence, etc.
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([locationId])
|
|
@@index([userId])
|
|
@@index([createdAt])
|
|
@@map("location_history")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — SHIFTS
|
|
// ============================================================================
|
|
|
|
enum ShiftStatus {
|
|
OPEN
|
|
FULL
|
|
CANCELLED
|
|
}
|
|
|
|
enum RecurrenceFrequency {
|
|
DAILY
|
|
WEEKLY
|
|
MONTHLY
|
|
}
|
|
|
|
model Shift {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
date DateTime @db.Date
|
|
startTime String // HH:MM format
|
|
endTime String // HH:MM format
|
|
location String?
|
|
maxVolunteers Int
|
|
currentVolunteers Int @default(0)
|
|
status ShiftStatus @default(OPEN)
|
|
isPublic Boolean @default(false)
|
|
cutId String?
|
|
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
|
|
|
|
// Repeating shift series
|
|
seriesId String?
|
|
series ShiftSeries? @relation(fields: [seriesId], references: [id], onDelete: SetNull)
|
|
isException Boolean @default(false)
|
|
|
|
// Gancio event sync
|
|
gancioEventId Int?
|
|
|
|
// Video briefing meeting
|
|
meetingId String? @unique
|
|
meeting Meeting? @relation("ShiftMeeting", fields: [meetingId], references: [id], onDelete: SetNull)
|
|
|
|
createdBy String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
signups ShiftSignup[]
|
|
canvassVisits CanvassVisit[]
|
|
canvassSessions CanvassSession[]
|
|
|
|
@@index([cutId])
|
|
@@index([seriesId])
|
|
@@map("shifts")
|
|
}
|
|
|
|
enum SignupStatus {
|
|
CONFIRMED
|
|
CANCELLED
|
|
}
|
|
|
|
enum SignupSource {
|
|
AUTHENTICATED
|
|
PUBLIC
|
|
ADMIN
|
|
}
|
|
|
|
model ShiftSignup {
|
|
id String @id @default(cuid())
|
|
shiftId String
|
|
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
|
shiftTitle String?
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
userEmail String
|
|
userName String?
|
|
userPhone String?
|
|
signupDate DateTime @default(now())
|
|
status SignupStatus @default(CONFIRMED)
|
|
signupSource SignupSource @default(AUTHENTICATED)
|
|
|
|
@@unique([shiftId, userEmail])
|
|
@@index([shiftId])
|
|
@@map("shift_signups")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — SHIFT SERIES (Repeating Shifts)
|
|
// ============================================================================
|
|
|
|
model ShiftSeries {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
startTime String // HH:MM format
|
|
endTime String // HH:MM format
|
|
location String?
|
|
maxVolunteers Int
|
|
isPublic Boolean @default(false)
|
|
cutId String?
|
|
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
|
|
|
|
// Recurrence rules
|
|
frequency RecurrenceFrequency
|
|
daysOfWeek Json? // Array of day numbers: [1,3,5] for Mon/Wed/Fri
|
|
startDate DateTime @db.Date
|
|
endDate DateTime? @db.Date
|
|
|
|
// Metadata
|
|
createdBy String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
shifts Shift[]
|
|
|
|
@@index([cutId])
|
|
@@map("shift_series")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — CUTS (Geographic Polygon Overlays)
|
|
// ============================================================================
|
|
|
|
enum CutCategory {
|
|
CUSTOM
|
|
WARD
|
|
NEIGHBORHOOD
|
|
DISTRICT
|
|
}
|
|
|
|
model Cut {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String? @db.Text
|
|
color String @default("#3388ff")
|
|
opacity Decimal @default(0.3) @db.Decimal(3, 2)
|
|
category CutCategory?
|
|
isPublic Boolean @default(false)
|
|
isOfficial Boolean @default(false)
|
|
geojson String @db.Text // GeoJSON polygon data
|
|
bounds String? @db.Text // Bounding box JSON
|
|
showLocations Boolean @default(true)
|
|
exportEnabled Boolean @default(true)
|
|
assignedTo String?
|
|
filterSettings Json? // JSON filter configuration
|
|
lastCanvassed DateTime?
|
|
completionPercentage Int @default(0)
|
|
|
|
createdByUserId String?
|
|
createdByUser User? @relation("CutCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
shifts Shift[]
|
|
shiftSeries ShiftSeries[]
|
|
canvassSessions CanvassSession[]
|
|
|
|
@@map("cuts")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — SETTINGS
|
|
// ============================================================================
|
|
|
|
model MapSettings {
|
|
id String @id @default(cuid())
|
|
latitude Decimal? @db.Decimal(10, 8)
|
|
longitude Decimal? @db.Decimal(11, 8)
|
|
zoom Int?
|
|
walkSheetTitle String?
|
|
walkSheetSubtitle String?
|
|
walkSheetFooter String? @db.Text
|
|
qrCode1Url String?
|
|
qrCode1Label String?
|
|
qrCode2Url String?
|
|
qrCode2Label String?
|
|
qrCode3Url String?
|
|
qrCode3Label String?
|
|
publicMapEnabled Boolean @default(true)
|
|
publicShowLocations Boolean @default(true)
|
|
publicShowSupportLevels Boolean @default(true)
|
|
publicShowCuts Boolean @default(true)
|
|
publicShowEvents Boolean @default(true)
|
|
publicShowAddresses Boolean @default(true)
|
|
publicShowSignInfo Boolean @default(true)
|
|
createdBy String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("map_settings")
|
|
}
|
|
|
|
// ============================================================================
|
|
// SITE SETTINGS (Singleton — branding, theme, feature toggles)
|
|
// ============================================================================
|
|
|
|
model SiteSettings {
|
|
id String @id @default(cuid())
|
|
|
|
// Organization
|
|
organizationName String @default("Changemaker Lite")
|
|
organizationShortName String @default("CML")
|
|
organizationLogoUrl String?
|
|
organizationFaviconUrl String?
|
|
|
|
// Admin theme
|
|
adminColorPrimary String @default("#9d4edd")
|
|
adminColorBgBase String @default("#1a1025")
|
|
|
|
// Public theme
|
|
publicColorPrimary String @default("#3498db")
|
|
publicColorBgBase String @default("#0d1b2a")
|
|
publicColorBgContainer String @default("#1b2838")
|
|
publicHeaderGradient String @default("linear-gradient(135deg, #005a9c 0%, #007acc 100%)")
|
|
|
|
// Text
|
|
footerText String @default("Powered by Changemaker Lite")
|
|
loginSubtitle String @default("Admin")
|
|
homepageTagline String? @map("homepage_tagline")
|
|
|
|
// Email branding
|
|
emailFromName String @default("Changemaker Lite")
|
|
|
|
// SMTP configuration (overrides env vars when set; empty/0 = use env fallback)
|
|
smtpHost String @default("")
|
|
smtpPort Int @default(0)
|
|
smtpUser String @default("")
|
|
smtpPass String @default("")
|
|
smtpFromAddress String @default("")
|
|
smtpActiveProvider String @default("mailhog") // "mailhog" | "production"
|
|
emailTestMode Boolean @default(true)
|
|
testEmailRecipient String @default("")
|
|
|
|
// Registration settings
|
|
enablePublicRegistration Boolean @default(true)
|
|
enableEmailVerification Boolean @default(true)
|
|
autoApproveVerifiedUsers Boolean @default(true)
|
|
|
|
// Feature toggles
|
|
enableInfluence Boolean @default(true)
|
|
enableMap Boolean @default(true)
|
|
enableNewsletter Boolean @default(true)
|
|
enableLandingPages Boolean @default(true)
|
|
enableMediaFeatures Boolean @default(true) @map("enable_media_features")
|
|
enablePayments Boolean @default(false)
|
|
enableGalleryAds Boolean @default(false) @map("enable_gallery_ads")
|
|
enableChat Boolean @default(false) @map("enable_chat")
|
|
enableEvents Boolean @default(false) @map("enable_events")
|
|
enableDocsComments Boolean @default(false) @map("enable_docs_comments")
|
|
enableSms Boolean @default(false) @map("enable_sms")
|
|
enablePeople Boolean @default(false) @map("enable_people")
|
|
enableSocial Boolean @default(false) @map("enable_social")
|
|
enableMeet Boolean @default(false) @map("enable_meet")
|
|
autoSyncPeopleToMap Boolean @default(false) @map("auto_sync_people_to_map")
|
|
|
|
// SMS connection config (overrides env vars when non-empty)
|
|
smsTermuxApiUrl String @default("") @map("sms_termux_api_url")
|
|
smsTermuxApiKey String @default("") @map("sms_termux_api_key") // Encrypted at rest
|
|
smsTailscaleApiKey String @default("") @map("sms_tailscale_api_key") // Encrypted at rest
|
|
smsTailscaleTailnet String @default("") @map("sms_tailscale_tailnet")
|
|
smsTailscaleDeviceId String @default("") @map("sms_tailscale_device_id")
|
|
smsTailscaleDeviceName String @default("") @map("sms_tailscale_device_name")
|
|
|
|
// Gitea Docs Comments (overrides env vars when set; empty = use env fallback)
|
|
giteaApiToken String @default("") // Encrypted at rest — Personal Access Token
|
|
giteaCommentsRepoOwner String @default("")
|
|
giteaCommentsRepoName String @default("docs-comments")
|
|
giteaOauthClientId String @default("")
|
|
giteaOauthClientSecret String @default("") // Encrypted at rest
|
|
|
|
// Notification settings
|
|
notifyAdminShiftSignup Boolean @default(true)
|
|
notifyAdminResponseSubmitted Boolean @default(true)
|
|
notifyAdminSignRequested Boolean @default(true)
|
|
notifyAdminShiftCancellation Boolean @default(true)
|
|
notifyVolunteerSessionSummary Boolean @default(true)
|
|
notifyVolunteerCancellation Boolean @default(true)
|
|
notifyVolunteerShiftReminder Boolean @default(true)
|
|
notifyVolunteerShiftThankYou Boolean @default(true)
|
|
|
|
// SMS notification settings
|
|
smsShiftReminders Boolean @default(false) @map("sms_shift_reminders")
|
|
smsShiftReminderHours Int @default(24) @map("sms_shift_reminder_hours")
|
|
smsShiftSignupConfirm Boolean @default(false) @map("sms_shift_signup_confirm")
|
|
smsVolunteerWelcome Boolean @default(false) @map("sms_volunteer_welcome")
|
|
|
|
// Re-engagement settings
|
|
notifyVolunteerReengagement Boolean @default(false) @map("notify_volunteer_reengagement")
|
|
reengagementInactiveDays Int @default(30) @map("reengagement_inactive_days")
|
|
reengagementCooldownDays Int @default(30) @map("reengagement_cooldown_days")
|
|
|
|
// Navigation configuration (JSON: { items: NavItem[] })
|
|
navConfig Json? @map("nav_config")
|
|
|
|
// User Provisioning (centralized user management across services)
|
|
enableUserProvisioning Boolean @default(false) @map("enable_user_provisioning")
|
|
provisionGitea Boolean @default(false) @map("provision_gitea")
|
|
provisionGiteaTiming String @default("lazy") @map("provision_gitea_timing") // 'lazy' | 'eager'
|
|
provisionVaultwarden Boolean @default(false) @map("provision_vaultwarden")
|
|
provisionVaultwardenTiming String @default("lazy") @map("provision_vaultwarden_timing") // 'lazy' | 'eager'
|
|
provisionListmonk Boolean @default(true) @map("provision_listmonk")
|
|
provisionListmonkTiming String @default("eager") @map("provision_listmonk_timing") // 'lazy' | 'eager'
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("site_settings")
|
|
}
|
|
|
|
// ============================================================================
|
|
// EMAIL TEMPLATES
|
|
// ============================================================================
|
|
|
|
enum EmailTemplateCategory {
|
|
INFLUENCE
|
|
MAP
|
|
SYSTEM
|
|
PAYMENT
|
|
}
|
|
|
|
enum EmailTemplateVariableType {
|
|
TEXT
|
|
VIDEO
|
|
}
|
|
|
|
model EmailTemplate {
|
|
id String @id @default(cuid())
|
|
key String @unique // e.g., "campaign-email"
|
|
name String // Display name
|
|
description String? @db.Text
|
|
category EmailTemplateCategory // INFLUENCE | MAP | SYSTEM
|
|
subjectLine String // Template with {{VAR}} support
|
|
htmlContent String @db.Text
|
|
textContent String @db.Text
|
|
isSystem Boolean @default(false) // Prevent deletion
|
|
isActive Boolean @default(true)
|
|
|
|
variables EmailTemplateVariable[]
|
|
versions EmailTemplateVersion[]
|
|
testLogs EmailTemplateTestLog[]
|
|
|
|
createdByUserId String
|
|
createdBy User @relation("TemplatesCreated", fields: [createdByUserId], references: [id])
|
|
updatedByUserId String?
|
|
updatedBy User? @relation("TemplatesUpdated", fields: [updatedByUserId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([category])
|
|
@@index([isActive])
|
|
@@map("email_templates")
|
|
}
|
|
|
|
model EmailTemplateVariable {
|
|
id String @id @default(cuid())
|
|
templateId String
|
|
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
|
|
key String // e.g., "USER_NAME"
|
|
label String // e.g., "User Name"
|
|
description String? @db.Text // e.g., "Name of the user sending the email"
|
|
type EmailTemplateVariableType @default(TEXT) // TEXT | VIDEO
|
|
videoId Int? // Optional FK to videos (not enforced, separate Media API DB)
|
|
isRequired Boolean @default(true)
|
|
isConditional Boolean @default(false) // Used in {{#if}} blocks
|
|
sampleValue String? @db.Text // e.g., "John Doe"
|
|
sortOrder Int @default(0)
|
|
|
|
@@unique([templateId, key])
|
|
@@index([templateId])
|
|
@@index([type])
|
|
@@map("email_template_variables")
|
|
}
|
|
|
|
model EmailTemplateVersion {
|
|
id String @id @default(cuid())
|
|
templateId String
|
|
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
|
|
versionNumber Int // Auto-increment per template
|
|
subjectLine String
|
|
htmlContent String @db.Text
|
|
textContent String @db.Text
|
|
changeNotes String? @db.Text
|
|
|
|
createdByUserId String
|
|
createdBy User @relation("TemplateVersionsCreated", fields: [createdByUserId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([templateId, versionNumber])
|
|
@@index([templateId, createdAt(sort: Desc)])
|
|
@@map("email_template_versions")
|
|
}
|
|
|
|
model EmailTemplateTestLog {
|
|
id String @id @default(cuid())
|
|
templateId String
|
|
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
|
|
recipientEmail String
|
|
testData Json // Sample variable values used
|
|
success Boolean
|
|
errorMessage String? @db.Text
|
|
messageId String? // Nodemailer message ID
|
|
|
|
sentByUserId String
|
|
sentBy User @relation("TemplateTestsSent", fields: [sentByUserId], references: [id])
|
|
sentAt DateTime @default(now())
|
|
|
|
@@index([templateId, sentAt(sort: Desc)])
|
|
@@map("email_template_test_logs")
|
|
}
|
|
|
|
// ============================================================================
|
|
// LANDING PAGES
|
|
// ============================================================================
|
|
|
|
enum EditorMode {
|
|
VISUAL
|
|
CODE
|
|
}
|
|
|
|
enum MkdocsExportMode {
|
|
THEMED // extends main.html, content block only
|
|
STANDALONE // full HTML document, no Jinja2 inheritance
|
|
}
|
|
|
|
model LandingPage {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
title String
|
|
description String? @db.Text
|
|
blocks Json // JSON from GrapesJS editor
|
|
htmlOutput String? @db.Text
|
|
cssOutput String? @db.Text
|
|
editorMode EditorMode @default(VISUAL)
|
|
mkdocsPath String? // Path in mkdocs/overrides/
|
|
mkdocsStubPath String? // Path to .md stub in mkdocs/docs/
|
|
mkdocsExportMode MkdocsExportMode @default(THEMED)
|
|
mkdocsHideNav Boolean @default(true)
|
|
mkdocsHideToc Boolean @default(true)
|
|
mkdocsSkipExport Boolean @default(false)
|
|
published Boolean @default(false)
|
|
listed Boolean @default(false)
|
|
seoTitle String?
|
|
seoDescription String? @db.Text
|
|
seoImage String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("landing_pages")
|
|
}
|
|
|
|
model PageBlock {
|
|
id String @id @default(cuid())
|
|
type String // hero, text, image, cta, features, testimonials, form
|
|
label String
|
|
schema Json // Block configuration schema
|
|
defaults Json // Default values
|
|
thumbnail String?
|
|
category String?
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("page_blocks")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — CANVASSING
|
|
// ============================================================================
|
|
|
|
enum VisitOutcome {
|
|
NOT_HOME
|
|
REFUSED
|
|
MOVED
|
|
ALREADY_VOTED
|
|
SPOKE_WITH
|
|
LEFT_LITERATURE
|
|
COME_BACK_LATER
|
|
}
|
|
|
|
enum CanvassSessionStatus {
|
|
ACTIVE
|
|
COMPLETED
|
|
ABANDONED
|
|
}
|
|
|
|
model CanvassSession {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation("CanvassSessions", fields: [userId], references: [id], onDelete: Cascade)
|
|
cutId String
|
|
cut Cut @relation(fields: [cutId], references: [id], onDelete: Cascade)
|
|
shiftId String?
|
|
shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull)
|
|
status CanvassSessionStatus @default(ACTIVE)
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
startLatitude Decimal? @db.Decimal(10, 8)
|
|
startLongitude Decimal? @db.Decimal(11, 8)
|
|
|
|
visits CanvassVisit[]
|
|
trackingSession TrackingSession?
|
|
|
|
@@index([userId])
|
|
@@index([cutId])
|
|
@@index([shiftId])
|
|
@@map("canvass_sessions")
|
|
}
|
|
|
|
model CanvassVisit {
|
|
id String @id @default(cuid())
|
|
addressId String // Changed from locationId
|
|
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade)
|
|
userId String
|
|
user User @relation("CanvassVisitor", fields: [userId], references: [id], onDelete: Cascade)
|
|
shiftId String?
|
|
shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull)
|
|
sessionId String?
|
|
session CanvassSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
|
outcome VisitOutcome
|
|
supportLevel SupportLevel?
|
|
signRequested Boolean @default(false)
|
|
signSize String?
|
|
notes String? @db.Text
|
|
durationSeconds Int?
|
|
visitedAt DateTime @default(now())
|
|
|
|
@@index([addressId]) // Changed from locationId
|
|
@@index([addressId, visitedAt(sort: Desc)]) // For distinct + orderBy queries
|
|
@@index([userId])
|
|
@@index([shiftId])
|
|
@@index([sessionId])
|
|
@@index([visitedAt])
|
|
@@map("canvass_visits")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAP — GPS TRACKING
|
|
// ============================================================================
|
|
|
|
enum TrackPointEvent {
|
|
LOCATION_ADDED
|
|
VISIT_RECORDED
|
|
SESSION_STARTED
|
|
SESSION_ENDED
|
|
}
|
|
|
|
model TrackingSession {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation("TrackingSessions", fields: [userId], references: [id], onDelete: Cascade)
|
|
canvassSessionId String? @unique
|
|
canvassSession CanvassSession? @relation(fields: [canvassSessionId], references: [id], onDelete: SetNull)
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
isActive Boolean @default(true)
|
|
totalPoints Int @default(0)
|
|
totalDistanceM Float @default(0)
|
|
lastLatitude Decimal? @db.Decimal(10, 8)
|
|
lastLongitude Decimal? @db.Decimal(11, 8)
|
|
lastRecordedAt DateTime?
|
|
trackPoints TrackPoint[]
|
|
|
|
@@index([userId])
|
|
@@index([isActive])
|
|
@@index([isActive, lastRecordedAt])
|
|
@@map("tracking_sessions")
|
|
}
|
|
|
|
model TrackPoint {
|
|
id String @id @default(cuid())
|
|
trackingSessionId String
|
|
trackingSession TrackingSession @relation(fields: [trackingSessionId], references: [id], onDelete: Cascade)
|
|
latitude Decimal @db.Decimal(10, 8)
|
|
longitude Decimal @db.Decimal(11, 8)
|
|
accuracy Float?
|
|
recordedAt DateTime @default(now())
|
|
eventType TrackPointEvent?
|
|
|
|
@@index([trackingSessionId, recordedAt])
|
|
@@index([recordedAt])
|
|
@@map("track_points")
|
|
}
|
|
// Enums
|
|
enum DirectoryType {
|
|
studios
|
|
gifs
|
|
private
|
|
inbox
|
|
curated
|
|
playback
|
|
compilations
|
|
videos
|
|
highlights
|
|
}
|
|
|
|
enum ResourceCategory {
|
|
gpu_ai
|
|
gpu_encode
|
|
cpu
|
|
}
|
|
|
|
enum JobStatus {
|
|
pending
|
|
queued
|
|
running
|
|
completed
|
|
failed
|
|
cancelled
|
|
}
|
|
|
|
enum ReactionType {
|
|
like
|
|
love
|
|
laugh
|
|
wow
|
|
sad
|
|
angry
|
|
}
|
|
|
|
enum WordFilterLevel {
|
|
low
|
|
medium
|
|
high
|
|
custom
|
|
}
|
|
|
|
enum ReportType {
|
|
inappropriate
|
|
spam
|
|
copyright
|
|
illegal
|
|
false_info
|
|
other
|
|
}
|
|
|
|
enum ReportStatus {
|
|
pending
|
|
reviewed
|
|
actioned
|
|
dismissed
|
|
}
|
|
|
|
enum FriendshipStatus {
|
|
pending
|
|
accepted
|
|
declined
|
|
}
|
|
|
|
enum SocialPlatform {
|
|
twitter
|
|
instagram
|
|
onlyfans
|
|
fansly
|
|
reddit
|
|
discord
|
|
tiktok
|
|
youtube
|
|
twitch
|
|
snapchat
|
|
linktree
|
|
custom
|
|
}
|
|
|
|
enum UserUploadStatus {
|
|
pending
|
|
approved
|
|
rejected
|
|
}
|
|
|
|
enum UploadInviteStatus {
|
|
active
|
|
inactive
|
|
expired
|
|
}
|
|
|
|
enum DigestStatus {
|
|
pending
|
|
scene_detection
|
|
extracting
|
|
analyzing
|
|
face_detection
|
|
transcribing
|
|
segmenting
|
|
synthesizing
|
|
completed
|
|
failed
|
|
cancelled
|
|
}
|
|
|
|
enum ClipType {
|
|
hook
|
|
intro
|
|
action
|
|
climax
|
|
highlight
|
|
}
|
|
|
|
enum ClipStatus {
|
|
pending
|
|
processing
|
|
completed
|
|
failed
|
|
}
|
|
|
|
enum ClipSource {
|
|
machine
|
|
manual
|
|
}
|
|
|
|
enum CaptionPosition {
|
|
bottom
|
|
top
|
|
}
|
|
|
|
enum CaptionSize {
|
|
small
|
|
medium
|
|
large
|
|
}
|
|
|
|
enum SegmentType {
|
|
scene
|
|
tag_change
|
|
vocal
|
|
fixed_interval
|
|
}
|
|
|
|
enum VocalCategory {
|
|
dialogue
|
|
dirty_talk
|
|
climax
|
|
interview
|
|
moan
|
|
}
|
|
|
|
enum SuggestedTagStatus {
|
|
pending
|
|
approved
|
|
rejected
|
|
mapped
|
|
}
|
|
|
|
enum WatchPartyStatus {
|
|
active
|
|
completed
|
|
cancelled
|
|
}
|
|
|
|
enum PipelineStatus {
|
|
draft
|
|
active
|
|
paused
|
|
completed
|
|
failed
|
|
}
|
|
|
|
enum PipelineStepStatus {
|
|
pending
|
|
running
|
|
completed
|
|
failed
|
|
skipped
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
none
|
|
active
|
|
grace_period
|
|
delinquent
|
|
lifetime
|
|
cancelled
|
|
}
|
|
|
|
enum InvoiceStatus {
|
|
pending
|
|
paid
|
|
failed
|
|
refunded
|
|
}
|
|
|
|
enum PaymentStatus {
|
|
pending
|
|
succeeded
|
|
failed
|
|
refunded
|
|
}
|
|
|
|
enum PaymentMethod {
|
|
card
|
|
bank_transfer
|
|
crypto
|
|
stripe
|
|
}
|
|
|
|
enum ProductType {
|
|
DIGITAL
|
|
EVENT
|
|
DONATION
|
|
}
|
|
|
|
enum OrderStatus {
|
|
PENDING
|
|
COMPLETED
|
|
FAILED
|
|
REFUNDED
|
|
}
|
|
|
|
enum NotificationType {
|
|
friend_request
|
|
friend_accepted
|
|
poke
|
|
comment
|
|
upload_approved
|
|
upload_rejected
|
|
achievement
|
|
system
|
|
group_call
|
|
}
|
|
|
|
// ============================================================================
|
|
// CORE VIDEO LIBRARY
|
|
// ============================================================================
|
|
|
|
model Video {
|
|
id Int @id @default(autoincrement())
|
|
path String @unique
|
|
filename String
|
|
producer String?
|
|
creator String?
|
|
title String?
|
|
durationSeconds Int? @map("duration_seconds")
|
|
quality String?
|
|
orientation String?
|
|
hasAudio Boolean? @default(true) @map("has_audio")
|
|
fileSize BigInt? @map("file_size")
|
|
fileHash String? @map("file_hash")
|
|
width Int?
|
|
height Int?
|
|
lastValidated DateTime? @map("last_validated")
|
|
isValid Boolean? @default(true) @map("is_valid")
|
|
thumbnailPath String? @map("thumbnail_path")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
tags Json?
|
|
directoryType DirectoryType? @map("directory_type")
|
|
// Historical engagement stats
|
|
publicViewCount Int? @map("public_view_count")
|
|
publicUpvoteCount Int? @map("public_upvote_count")
|
|
publicCommentCount Int? @map("public_comment_count")
|
|
publicCompletionCount Int? @map("public_completion_count")
|
|
publicTotalWatchTime Int? @map("public_total_watch_time")
|
|
movedFromPublicAt DateTime? @map("moved_from_public_at")
|
|
// Name standardization tracking
|
|
originalFilename String? @map("original_filename")
|
|
originalPath String? @map("original_path")
|
|
standardizedAt DateTime? @map("standardized_at")
|
|
|
|
// Publishing system (replaces PublicMedia)
|
|
isPublished Boolean @default(false) @map("is_published")
|
|
publishedAt DateTime? @map("published_at")
|
|
category String? // videos|curated|compilations|playback|highlights
|
|
isShort Boolean @default(false) @map("is_short")
|
|
|
|
// Moderation system
|
|
isLocked Boolean @default(false) @map("is_locked")
|
|
lockedAt DateTime? @map("locked_at")
|
|
lockedById String? @map("locked_by_id")
|
|
|
|
// Engagement counters
|
|
viewCount Int @default(0) @map("view_count")
|
|
upvoteCount Int @default(0) @map("upvote_count")
|
|
commentCount Int @default(0) @map("comment_count")
|
|
finishCount Int @default(0) @map("finish_count")
|
|
totalWatchTime Int @default(0) @map("total_watch_time")
|
|
|
|
// Scheduled publishing
|
|
scheduledPublishAt DateTime? @map("scheduled_publish_at")
|
|
scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at")
|
|
|
|
// Enhanced analytics
|
|
uniqueViewers Int @default(0) @map("unique_viewers")
|
|
totalWatchTimeSeconds Int @default(0) @map("total_watch_time_seconds")
|
|
averageWatchTimeSeconds Decimal @default(0) @map("average_watch_time_seconds") @db.Decimal(10, 2)
|
|
completionRate Decimal @default(0) @map("completion_rate") @db.Decimal(5, 2)
|
|
|
|
// Content gating
|
|
accessLevel String @default("free") @map("access_level") // free|member|premium
|
|
|
|
// Ordering
|
|
position Int? @default(0)
|
|
|
|
// Uploader tracking
|
|
uploaderId String? @map("uploader_id")
|
|
|
|
// Relations
|
|
uploader User? @relation("VideoUploader", fields: [uploaderId], references: [id])
|
|
locker User? @relation("VideoLocker", fields: [lockedById], references: [id])
|
|
upvotes Upvote[]
|
|
comments Comment[]
|
|
views View[]
|
|
reactions VideoReaction[]
|
|
finishes UserFinish[]
|
|
contentReports ContentReport[]
|
|
playlistVideos PlaylistVideo[]
|
|
publicMediaTags PublicMediaTag[] @relation("PublicMediaTags")
|
|
publicMediaPerformers PublicMediaPerformer[] @relation("PublicMediaPerformers")
|
|
recommendations VideoRecommendation[]
|
|
videoDigests VideoDigest[]
|
|
digestVideoTags DigestVideoTag[]
|
|
digestSelectedClips DigestSelectedClip[]
|
|
digestGeneratedScenes DigestGeneratedScene[]
|
|
digestOutputFolders DigestOutputFolder[]
|
|
digestCompilations DigestCompilation[]
|
|
videoSceneCuts VideoSceneCut[]
|
|
videoTagTimeline VideoTagTimeline[]
|
|
videoSegments VideoSegment[]
|
|
videoTags VideoTag[]
|
|
digestClipTags DigestClipTag[]
|
|
tagGenerationJobs TagGenerationJob[]
|
|
videoOcrResults VideoOcrResult[]
|
|
videoViews VideoView[]
|
|
videoEvents VideoEvent[]
|
|
scheduleHistory VideoScheduleHistory[]
|
|
|
|
@@index([orientation], map: "idx_orientation")
|
|
@@index([producer], map: "idx_producer")
|
|
@@index([isValid], map: "idx_is_valid")
|
|
@@index([directoryType], map: "idx_directory_type")
|
|
@@index([durationSeconds, fileSize, width, height], map: "idx_videos_fingerprint")
|
|
@@index([directoryType, isValid, orientation], map: "idx_videos_directory_valid_orientation")
|
|
@@index([isPublished, isLocked], map: "idx_videos_published_locked")
|
|
@@index([category, isPublished], map: "idx_videos_category_published")
|
|
@@index([isShort, isPublished, isLocked], map: "idx_videos_short_published")
|
|
@@index([uploaderId], map: "idx_videos_uploader")
|
|
@@map("videos")
|
|
}
|
|
|
|
model Compilation {
|
|
id Int @id @default(autoincrement())
|
|
filename String
|
|
path String?
|
|
durationSeconds Int? @map("duration_seconds")
|
|
videoIds Json? @map("video_ids")
|
|
settings Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("compilations")
|
|
}
|
|
|
|
model Job {
|
|
id Int @id @default(autoincrement())
|
|
type String
|
|
status JobStatus? @default(pending)
|
|
progress Int? @default(0)
|
|
log String?
|
|
params Json?
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
// Queue management
|
|
resourceCategory ResourceCategory? @default(cpu) @map("resource_category")
|
|
vramRequired Int? @default(0) @map("vram_required")
|
|
queuePosition Int? @map("queue_position")
|
|
waitingReason String? @map("waiting_reason")
|
|
priority Int? @default(5)
|
|
// Pipeline integration
|
|
pipelineId Int? @map("pipeline_id")
|
|
pipelineStepId Int? @map("pipeline_step_id")
|
|
|
|
// Relations
|
|
pipeline Pipeline? @relation(fields: [pipelineId], references: [id])
|
|
pipelineStep PipelineStep? @relation(fields: [pipelineStepId], references: [id])
|
|
videoDigest VideoDigest?
|
|
tagGenerationJobs TagGenerationJob[]
|
|
|
|
@@index([status, priority, createdAt], map: "idx_jobs_queue")
|
|
@@index([resourceCategory, status], map: "idx_jobs_resource")
|
|
@@index([pipelineId], map: "idx_jobs_pipeline")
|
|
@@map("jobs")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PUBLIC GALLERY (DEPRECATED - Consolidated into Video model)
|
|
// ============================================================================
|
|
|
|
// NOTE: PublicMedia model has been removed - all functionality consolidated into Video model
|
|
// with isPublished, category, isLocked, and engagement counter fields.
|
|
// Migration path: All existing PublicMedia data should be migrated to Video table with
|
|
// isPublished=true before dropping this table.
|
|
|
|
// COMMENTED OUT - SCHEDULED FOR DELETION AFTER DATA MIGRATION
|
|
// model PublicMedia {
|
|
// id Int @id @default(autoincrement())
|
|
// path String @unique
|
|
// filename String
|
|
// category String
|
|
// durationSeconds Int? @map("duration_seconds")
|
|
// quality String?
|
|
// orientation String?
|
|
// thumbnailPath String? @map("thumbnail_path")
|
|
// fileSize BigInt? @map("file_size")
|
|
// viewCount Int? @default(0) @map("view_count")
|
|
// upvoteCount Int? @default(0) @map("upvote_count")
|
|
// commentCount Int? @default(0) @map("comment_count")
|
|
// finishCount Int? @default(0) @map("finish_count")
|
|
// totalWatchTime Int? @default(0) @map("total_watch_time")
|
|
// createdAt DateTime @default(now()) @map("created_at")
|
|
// isLocked Boolean? @default(false) @map("is_locked")
|
|
// lockedAt DateTime? @map("locked_at")
|
|
// lockedBy String? @map("locked_by")
|
|
// position Int? @default(0)
|
|
// uploaderId String? @map("uploader_id")
|
|
//
|
|
// @@index([category], map: "idx_public_media_category")
|
|
// @@index([path], map: "idx_public_media_path")
|
|
// @@index([isLocked], map: "idx_public_media_is_locked")
|
|
// @@index([position], map: "idx_public_media_position")
|
|
// @@index([uploaderId], map: "idx_public_media_uploader")
|
|
// @@index([category, createdAt], map: "idx_public_media_category_date")
|
|
// @@index([orientation], map: "idx_public_media_orientation")
|
|
// @@index([category, isLocked, createdAt], map: "idx_public_media_category_locked_date")
|
|
// @@map("public_media")
|
|
// }
|
|
|
|
model Session {
|
|
id String @id
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
lastSeenAt DateTime? @map("last_seen_at")
|
|
// Device tracking
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent")
|
|
deviceType String? @map("device_type")
|
|
browser String?
|
|
os String?
|
|
// Geography
|
|
country String?
|
|
countryName String? @map("country_name")
|
|
region String?
|
|
city String?
|
|
timezone String?
|
|
latitude Float? @db.Real
|
|
longitude Float? @db.Real
|
|
// User correlation
|
|
userId String? @map("user_id")
|
|
// Analytics
|
|
firstSeenAt DateTime? @map("first_seen_at")
|
|
visitCount Int? @default(1) @map("visit_count")
|
|
|
|
// Relations
|
|
user User? @relation("SessionUser", fields: [userId], references: [id])
|
|
upvotes Upvote[]
|
|
comments Comment[]
|
|
views View[]
|
|
sessionBans SessionBan[]
|
|
contentReports ContentReport[]
|
|
playlistViews PlaylistView[]
|
|
adImpressions AdImpression[]
|
|
adClicks AdClick[]
|
|
userFinishes UserFinish[]
|
|
|
|
// Photo gallery relations
|
|
photoUpvotes PhotoUpvote[] @relation("SessionPhotoUpvotes")
|
|
photoComments PhotoComment[] @relation("SessionPhotoComments")
|
|
photoReactions PhotoReaction[] @relation("SessionPhotoReactions")
|
|
|
|
@@index([userId], map: "idx_sessions_user_id")
|
|
@@index([country], map: "idx_sessions_country")
|
|
@@map("sessions")
|
|
}
|
|
|
|
model Upvote {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
sessionId String @map("session_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
session Session @relation(fields: [sessionId], references: [id])
|
|
|
|
@@index([mediaId, sessionId], map: "idx_upvotes_unique")
|
|
@@index([mediaId], map: "idx_upvotes_media")
|
|
@@map("upvotes")
|
|
}
|
|
|
|
model Comment {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
sessionId String @map("session_id")
|
|
userId String? @map("user_id")
|
|
content String
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
// Content safety fields
|
|
safetyStatus String? @default("pending") @map("safety_status")
|
|
safetyCheckedAt DateTime? @map("safety_checked_at")
|
|
safetyCategories Json? @map("safety_categories")
|
|
safetyReasoning String? @map("safety_reasoning")
|
|
// Hidden comment fields
|
|
isHidden Boolean? @default(false) @map("is_hidden")
|
|
hiddenAt DateTime? @map("hidden_at")
|
|
hiddenReason String? @map("hidden_reason")
|
|
moderationNotes String? @map("moderation_notes")
|
|
|
|
// Relations
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
session Session @relation(fields: [sessionId], references: [id])
|
|
user User? @relation("CommentUser", fields: [userId], references: [id])
|
|
moderation CommentModeration?
|
|
watchPartyMessages WatchPartyChatMessage[]
|
|
|
|
@@index([mediaId], map: "idx_comments_media")
|
|
@@index([sessionId], map: "idx_comments_session")
|
|
@@index([userId], map: "idx_comments_user")
|
|
@@index([safetyStatus], map: "idx_comments_safety_status")
|
|
@@index([isHidden], map: "idx_comments_is_hidden")
|
|
@@map("comments")
|
|
}
|
|
|
|
model View {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
sessionId String @map("session_id")
|
|
watchTimeSeconds Int? @default(0) @map("watch_time_seconds")
|
|
lastUpdated DateTime @default(now()) @map("last_updated")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
session Session @relation(fields: [sessionId], references: [id])
|
|
|
|
@@index([mediaId, sessionId], map: "idx_views_unique")
|
|
@@index([mediaId], map: "idx_views_media")
|
|
@@map("views")
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER MANAGEMENT (EXCLUDING v2Users and mediaUsers)
|
|
// ============================================================================
|
|
|
|
model AuthToken {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
token String @unique
|
|
type String
|
|
expiresAt DateTime @map("expires_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("AuthTokenUser", fields: [userId], references: [id])
|
|
|
|
@@index([token], map: "idx_auth_tokens_token")
|
|
@@index([userId], map: "idx_auth_tokens_user")
|
|
@@map("auth_tokens")
|
|
}
|
|
|
|
model SessionBan {
|
|
id Int @id @default(autoincrement())
|
|
sessionId String @map("session_id")
|
|
reason String?
|
|
bannedBy String? @map("banned_by")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
expiresAt DateTime? @map("expires_at")
|
|
|
|
// Relations
|
|
session Session @relation(fields: [sessionId], references: [id])
|
|
banner User? @relation("SessionBanner", fields: [bannedBy], references: [id])
|
|
|
|
@@index([sessionId], map: "idx_session_bans_session")
|
|
@@map("session_bans")
|
|
}
|
|
|
|
model CommentModeration {
|
|
id Int @id @default(autoincrement())
|
|
commentId Int @unique @map("comment_id")
|
|
status String @default("pending")
|
|
moderatedBy String? @map("moderated_by")
|
|
moderatedAt DateTime? @map("moderated_at")
|
|
reason String?
|
|
|
|
// Relations
|
|
comment Comment @relation(fields: [commentId], references: [id])
|
|
moderator User? @relation("CommentModerator", fields: [moderatedBy], references: [id])
|
|
|
|
@@index([commentId], map: "idx_comment_moderation_comment")
|
|
@@index([status], map: "idx_comment_moderation_status")
|
|
@@map("comment_moderation")
|
|
}
|
|
|
|
model EmailVerificationToken {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
token String @unique
|
|
expiresAt DateTime @map("expires_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("EmailVerificationTokens", fields: [userId], references: [id])
|
|
|
|
@@index([token], map: "idx_email_verification_tokens_token")
|
|
@@index([userId], map: "idx_email_verification_tokens_user")
|
|
@@map("email_verification_tokens")
|
|
}
|
|
|
|
model PasswordResetToken {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
token String @unique
|
|
expiresAt DateTime @map("expires_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
usedAt DateTime? @map("used_at")
|
|
|
|
// Relations
|
|
user User @relation("PasswordResetTokens", fields: [userId], references: [id])
|
|
|
|
@@index([token], map: "idx_password_reset_tokens_token")
|
|
@@index([userId], map: "idx_password_reset_tokens_user")
|
|
@@map("password_reset_tokens")
|
|
}
|
|
|
|
model EmailChangeToken {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
newEmail String @map("new_email")
|
|
token String @unique
|
|
expiresAt DateTime @map("expires_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("EmailChangeTokens", fields: [userId], references: [id])
|
|
|
|
@@index([token], map: "idx_email_change_tokens_token")
|
|
@@index([userId], map: "idx_email_change_tokens_user")
|
|
@@map("email_change_tokens")
|
|
}
|
|
|
|
// ============================================================================
|
|
// ACHIEVEMENTS & DASHBOARD
|
|
// ============================================================================
|
|
|
|
model UserAchievement {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
achievementId String @map("achievement_id")
|
|
unlockedAt DateTime @map("unlocked_at")
|
|
progress Int? @default(0)
|
|
notified Boolean? @default(false)
|
|
|
|
// Relations
|
|
user User @relation("UserAchievements", fields: [userId], references: [id])
|
|
|
|
@@unique([userId, achievementId], map: "idx_user_achievements_unique")
|
|
@@index([userId], map: "idx_user_achievements_user")
|
|
@@map("user_achievements")
|
|
}
|
|
|
|
model UserStats {
|
|
id Int @id @default(autoincrement())
|
|
userId String @unique @map("user_id")
|
|
totalWatchTimeSeconds Int? @default(0) @map("total_watch_time_seconds")
|
|
totalVideosWatched Int? @default(0) @map("total_videos_watched")
|
|
totalUpvotesGiven Int? @default(0) @map("total_upvotes_given")
|
|
totalCommentsMade Int? @default(0) @map("total_comments_made")
|
|
totalFinishes Int? @default(0) @map("total_finishes")
|
|
currentDayStreak Int? @default(0) @map("current_day_streak")
|
|
longestDayStreak Int? @default(0) @map("longest_day_streak")
|
|
lastActiveDate String? @map("last_active_date")
|
|
longestSingleSession Int? @default(0) @map("longest_single_session")
|
|
categoriesCompleted Json? @map("categories_completed")
|
|
nightOwlCount Int? @default(0) @map("night_owl_count")
|
|
earlyBirdCount Int? @default(0) @map("early_bird_count")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
user User @relation("UserStats", fields: [userId], references: [id])
|
|
|
|
@@map("user_stats")
|
|
}
|
|
|
|
model UserFinish {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
mediaId Int? @map("media_id")
|
|
sessionId String? @map("session_id")
|
|
createdAt DateTime @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("UserFinishes", fields: [userId], references: [id])
|
|
media Video? @relation(fields: [mediaId], references: [id])
|
|
session Session? @relation(fields: [sessionId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_finishes_user")
|
|
@@index([createdAt], map: "idx_user_finishes_date")
|
|
@@map("user_finishes")
|
|
}
|
|
|
|
model VideoReaction {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
mediaId Int @map("media_id")
|
|
reactionType ReactionType @map("reaction_type")
|
|
videoTimestamp Int @map("video_timestamp")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("VideoReactions", fields: [userId], references: [id])
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
|
|
@@index([userId, mediaId, reactionType], map: "idx_video_reactions_user_media_type")
|
|
@@index([mediaId, videoTimestamp], map: "idx_video_reactions_media_timestamp")
|
|
@@index([mediaId], map: "idx_video_reactions_media")
|
|
@@index([createdAt], map: "idx_video_reactions_created")
|
|
@@map("video_reactions")
|
|
}
|
|
|
|
model HighlightCooldown {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
lastGeneratedAt DateTime @map("last_generated_at")
|
|
|
|
// Relations
|
|
user User @relation("HighlightCooldowns", fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_highlight_cooldowns_user")
|
|
@@map("highlight_cooldowns")
|
|
}
|
|
|
|
model UserDailyActivity {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
activityDate String @map("activity_date")
|
|
watchTimeSeconds Int? @default(0) @map("watch_time_seconds")
|
|
videosWatched Int? @default(0) @map("videos_watched")
|
|
firstActivityHour Int? @map("first_activity_hour")
|
|
createdAt DateTime? @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("UserDailyActivity", fields: [userId], references: [id])
|
|
|
|
@@index([userId, activityDate], map: "idx_user_daily_activity_unique")
|
|
@@map("user_daily_activity")
|
|
}
|
|
|
|
model ChatThreadReadStatus {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
mediaId Int @map("media_id")
|
|
lastSeenAt DateTime @map("last_seen_at")
|
|
|
|
// Relations
|
|
user User @relation("ChatThreadReadStatus", fields: [userId], references: [id])
|
|
|
|
@@index([userId, mediaId], map: "idx_chat_thread_read_unique")
|
|
@@index([userId], map: "idx_chat_thread_read_user")
|
|
@@map("chat_thread_read_status")
|
|
}
|
|
|
|
// ============================================================================
|
|
// APP SETTINGS & MODERATION
|
|
// ============================================================================
|
|
|
|
model AppSetting {
|
|
key String @id
|
|
value String
|
|
updatedAt DateTime @default(now()) @map("updated_at")
|
|
|
|
@@map("app_settings")
|
|
}
|
|
|
|
model RateLimit {
|
|
id Int @id @default(autoincrement())
|
|
key String @unique
|
|
count Int @default(1)
|
|
resetAt DateTime @map("reset_at")
|
|
|
|
@@unique([key], map: "idx_rate_limits_key")
|
|
@@index([resetAt], map: "idx_rate_limits_reset_at")
|
|
@@map("rate_limits")
|
|
}
|
|
|
|
model ModerationWordList {
|
|
id Int @id @default(autoincrement())
|
|
level WordFilterLevel
|
|
word String
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
createdBy String? @map("created_by")
|
|
|
|
// Relations
|
|
creator User? @relation("ModerationWordListCreator", fields: [createdBy], references: [id])
|
|
|
|
@@index([level], map: "idx_moderation_word_lists_level")
|
|
@@index([word], map: "idx_moderation_word_lists_word")
|
|
@@map("moderation_word_lists")
|
|
}
|
|
|
|
model ContentReport {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
sessionId String? @map("session_id")
|
|
userId String? @map("user_id")
|
|
reportType ReportType @map("report_type")
|
|
description String?
|
|
status ReportStatus @default(pending)
|
|
resolvedBy String? @map("resolved_by")
|
|
resolvedAt DateTime? @map("resolved_at")
|
|
resolutionNotes String? @map("resolution_notes")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
session Session? @relation(fields: [sessionId], references: [id])
|
|
user User? @relation("ContentReportUser", fields: [userId], references: [id])
|
|
resolver User? @relation("ContentReportResolver", fields: [resolvedBy], references: [id])
|
|
|
|
@@index([mediaId], map: "idx_content_reports_media")
|
|
@@index([status], map: "idx_content_reports_status")
|
|
@@index([sessionId], map: "idx_content_reports_session")
|
|
@@index([createdAt], map: "idx_content_reports_created")
|
|
@@map("content_reports")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLAYLISTS
|
|
// ============================================================================
|
|
|
|
model Playlist {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
name String
|
|
description String?
|
|
isPublic Boolean? @default(false) @map("is_public")
|
|
shareToken String? @unique @map("share_token")
|
|
thumbnailMediaId Int? @map("thumbnail_media_id")
|
|
videoCount Int? @default(0) @map("video_count")
|
|
totalDurationSeconds Int? @default(0) @map("total_duration_seconds")
|
|
viewCount Int? @default(0) @map("view_count")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
user User @relation("UserPlaylists", fields: [userId], references: [id])
|
|
videos PlaylistVideo[]
|
|
featured FeaturedPlaylist?
|
|
views PlaylistView[]
|
|
|
|
@@unique([userId, name], map: "idx_playlists_user_name")
|
|
@@index([userId], map: "idx_playlists_user")
|
|
@@index([isPublic], map: "idx_playlists_public")
|
|
@@index([shareToken], map: "idx_playlists_share_token")
|
|
@@map("playlists")
|
|
}
|
|
|
|
model PlaylistVideo {
|
|
id Int @id @default(autoincrement())
|
|
playlistId Int @map("playlist_id")
|
|
mediaId Int @map("media_id")
|
|
position Int @default(0)
|
|
addedAt DateTime @default(now()) @map("added_at")
|
|
|
|
// Relations
|
|
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
|
|
@@index([playlistId], map: "idx_playlist_videos_playlist")
|
|
@@index([mediaId], map: "idx_playlist_videos_media")
|
|
@@index([playlistId, mediaId], map: "idx_playlist_videos_unique")
|
|
@@map("playlist_videos")
|
|
}
|
|
|
|
model FeaturedPlaylist {
|
|
id Int @id @default(autoincrement())
|
|
playlistId Int @unique @map("playlist_id")
|
|
position Int @default(0)
|
|
featuredBy String? @map("featured_by")
|
|
featuredAt DateTime? @map("featured_at")
|
|
|
|
// Relations
|
|
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
|
|
featurer User? @relation("FeaturedPlaylistFeaturer", fields: [featuredBy], references: [id])
|
|
|
|
@@index([position], map: "idx_featured_playlists_position")
|
|
@@map("featured_playlists")
|
|
}
|
|
|
|
model PlaylistView {
|
|
id Int @id @default(autoincrement())
|
|
playlistId Int @map("playlist_id")
|
|
sessionId String @map("session_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
|
|
session Session @relation(fields: [sessionId], references: [id])
|
|
|
|
@@index([playlistId], map: "idx_playlist_views_playlist")
|
|
@@index([playlistId, sessionId], map: "idx_playlist_views_unique")
|
|
@@map("playlist_views")
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADVERTISEMENTS
|
|
// ============================================================================
|
|
|
|
model Ad {
|
|
id Int @id @default(autoincrement())
|
|
type String
|
|
variant String?
|
|
imagePath String? @map("image_path")
|
|
linkUrl String? @map("link_url")
|
|
title String?
|
|
subtitle String? @db.Text
|
|
ctaText String? @map("cta_text")
|
|
ctaStyle String? @default("primary") @map("cta_style")
|
|
bgColor String? @map("bg_color")
|
|
iconEmoji String? @map("icon_emoji")
|
|
isSystemAd Boolean @default(false) @map("is_system_ad")
|
|
frequency Int @default(6)
|
|
visibility String @default("everyone")
|
|
isActive Boolean? @default(true) @map("is_active")
|
|
position Int? @default(0)
|
|
impressionCount Int? @default(0) @map("impression_count")
|
|
clickCount Int? @default(0) @map("click_count")
|
|
startDate DateTime? @map("start_date")
|
|
endDate DateTime? @map("end_date")
|
|
placements Json? @default("[]")
|
|
productId String? @unique @map("product_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
impressions AdImpression[]
|
|
clicks AdClick[]
|
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([type], map: "idx_ads_type")
|
|
@@index([isActive], map: "idx_ads_is_active")
|
|
@@index([visibility], map: "idx_ads_visibility")
|
|
@@map("ads")
|
|
}
|
|
|
|
model AdImpression {
|
|
id Int @id @default(autoincrement())
|
|
adId Int @map("ad_id")
|
|
sessionId String? @map("session_id")
|
|
userId String? @map("user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade)
|
|
session Session? @relation(fields: [sessionId], references: [id])
|
|
user User? @relation("AdImpressionUser", fields: [userId], references: [id])
|
|
|
|
@@index([adId], map: "idx_ad_impressions_ad")
|
|
@@index([sessionId], map: "idx_ad_impressions_session")
|
|
@@index([createdAt], map: "idx_ad_impressions_date")
|
|
@@map("ad_impressions")
|
|
}
|
|
|
|
model AdClick {
|
|
id Int @id @default(autoincrement())
|
|
adId Int @map("ad_id")
|
|
sessionId String? @map("session_id")
|
|
userId String? @map("user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade)
|
|
session Session? @relation(fields: [sessionId], references: [id])
|
|
user User? @relation("AdClickUser", fields: [userId], references: [id])
|
|
|
|
@@index([adId], map: "idx_ad_clicks_ad")
|
|
@@index([sessionId], map: "idx_ad_clicks_session")
|
|
@@index([createdAt], map: "idx_ad_clicks_date")
|
|
@@map("ad_clicks")
|
|
}
|
|
|
|
// ============================================================================
|
|
// FRIENDS & SOCIAL
|
|
// ============================================================================
|
|
|
|
model Friendship {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
friendId String @map("friend_id")
|
|
status FriendshipStatus @default(pending)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
acceptedAt DateTime? @map("accepted_at")
|
|
|
|
// Relations
|
|
user User @relation("UserFriendships", fields: [userId], references: [id])
|
|
friend User @relation("UserFriends", fields: [friendId], references: [id])
|
|
|
|
@@index([userId, friendId], map: "idx_friendships_user_friend")
|
|
@@index([userId], map: "idx_friendships_user")
|
|
@@index([friendId], map: "idx_friendships_friend")
|
|
@@index([status], map: "idx_friendships_status")
|
|
@@map("friendships")
|
|
}
|
|
|
|
model UserBlock {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
blockedUserId String @map("blocked_user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("UserBlocks", fields: [userId], references: [id])
|
|
blockedUser User @relation("UserBlockedBy", fields: [blockedUserId], references: [id])
|
|
|
|
@@index([userId, blockedUserId], map: "idx_user_blocks_unique")
|
|
@@index([userId], map: "idx_user_blocks_user")
|
|
@@map("user_blocks")
|
|
}
|
|
|
|
model Poke {
|
|
id Int @id @default(autoincrement())
|
|
fromUserId String @map("from_user_id")
|
|
toUserId String @map("to_user_id")
|
|
isRead Boolean? @default(false) @map("is_read")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
from User @relation("PokesSent", fields: [fromUserId], references: [id])
|
|
to User @relation("PokesReceived", fields: [toUserId], references: [id])
|
|
|
|
@@index([toUserId], map: "idx_pokes_to_user")
|
|
@@index([fromUserId], map: "idx_pokes_from_user")
|
|
@@map("pokes")
|
|
}
|
|
|
|
model VideoRecommendation {
|
|
id Int @id @default(autoincrement())
|
|
fromUserId String @map("from_user_id")
|
|
toUserId String @map("to_user_id")
|
|
mediaId Int @map("media_id")
|
|
message String?
|
|
isRead Boolean? @default(false) @map("is_read")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
from User @relation("RecommendationsSent", fields: [fromUserId], references: [id])
|
|
to User @relation("RecommendationsReceived", fields: [toUserId], references: [id])
|
|
media Video @relation(fields: [mediaId], references: [id])
|
|
|
|
@@index([toUserId], map: "idx_video_recommendations_to_user")
|
|
@@index([fromUserId], map: "idx_video_recommendations_from_user")
|
|
@@index([mediaId], map: "idx_video_recommendations_media")
|
|
@@map("video_recommendations")
|
|
}
|
|
|
|
model UserPresence {
|
|
id Int @id @default(autoincrement())
|
|
userId String @unique @map("user_id")
|
|
isOnline Boolean? @default(false) @map("is_online")
|
|
currentMediaId Int? @map("current_media_id")
|
|
lastActivityAt DateTime? @map("last_activity_at")
|
|
lastVideoChangeAt DateTime? @map("last_video_change_at")
|
|
|
|
// Relations
|
|
user User @relation("UserPresence", fields: [userId], references: [id])
|
|
|
|
@@index([isOnline], map: "idx_user_presence_online")
|
|
@@index([userId], map: "idx_user_presence_user")
|
|
@@map("user_presence")
|
|
}
|
|
|
|
model UserGalleryImage {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
filename String
|
|
originalFilename String? @map("original_filename")
|
|
position Int @default(0)
|
|
uploadedAt DateTime @default(now()) @map("uploaded_at")
|
|
|
|
// Relations
|
|
user User @relation("UserGalleryImages", fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_gallery_user")
|
|
@@index([userId, position], map: "idx_user_gallery_position")
|
|
@@map("user_gallery_images")
|
|
}
|
|
|
|
model UserSocialLink {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
platform SocialPlatform
|
|
url String
|
|
displayName String? @map("display_name")
|
|
position Int @default(0)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("UserSocialLinks", fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_social_links_user")
|
|
@@index([userId, position], map: "idx_user_social_links_position")
|
|
@@map("user_social_links")
|
|
}
|
|
|
|
model PrivacySettings {
|
|
id Int @id @default(autoincrement())
|
|
userId String @unique @map("user_id")
|
|
showOnlineStatus Boolean? @default(true) @map("show_online_status")
|
|
showCurrentlyWatching Boolean? @default(true) @map("show_currently_watching")
|
|
showInFriendActivity Boolean? @default(true) @map("show_in_friend_activity")
|
|
anonymizePublicComments Boolean? @default(false) @map("anonymize_public_comments")
|
|
hidePublicReactions Boolean? @default(false) @map("hide_public_reactions")
|
|
hidePublicFinishes Boolean? @default(false) @map("hide_public_finishes")
|
|
allowFriendRequests Boolean? @default(true) @map("allow_friend_requests")
|
|
closeFriendsOnlyWatching Boolean? @default(false) @map("close_friends_only_watching")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
user User @relation("PrivacySettings", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId], map: "idx_privacy_settings_user")
|
|
@@map("privacy_settings")
|
|
}
|
|
|
|
model CloseFriend {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
closeFriendId String @map("close_friend_id")
|
|
addedAt DateTime @default(now()) @map("added_at")
|
|
|
|
// Relations
|
|
user User @relation("CloseFriends", fields: [userId], references: [id], onDelete: Cascade)
|
|
closeFriend User @relation("CloseFriendOf", fields: [closeFriendId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, closeFriendId], map: "idx_close_friends_unique")
|
|
@@index([userId], map: "idx_close_friends_user")
|
|
@@map("close_friends")
|
|
}
|
|
|
|
// ============================================================================
|
|
// SOCIAL GROUPS
|
|
// ============================================================================
|
|
|
|
enum SocialGroupType {
|
|
SHIFT_TEAM
|
|
CAMPAIGN_TEAM
|
|
CUSTOM
|
|
}
|
|
|
|
model SocialGroup {
|
|
id String @id @default(cuid())
|
|
name String
|
|
type SocialGroupType
|
|
referenceId String? @map("reference_id")
|
|
meetingId String? @unique @map("meeting_id")
|
|
meeting Meeting? @relation("GroupMeeting", fields: [meetingId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
members SocialGroupMember[]
|
|
|
|
@@unique([type, referenceId], map: "idx_social_groups_type_ref")
|
|
@@map("social_groups")
|
|
}
|
|
|
|
model SocialGroupMember {
|
|
id String @id @default(cuid())
|
|
groupId String @map("group_id")
|
|
group SocialGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
|
userId String @map("user_id")
|
|
user User @relation("SocialGroupMember", fields: [userId], references: [id], onDelete: Cascade)
|
|
joinedAt DateTime @default(now()) @map("joined_at")
|
|
|
|
@@unique([groupId, userId], map: "idx_social_group_members_unique")
|
|
@@index([userId], map: "idx_social_group_members_user")
|
|
@@map("social_group_members")
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER UPLOADS
|
|
// ============================================================================
|
|
|
|
model UserUpload {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
filename String
|
|
originalFilename String? @map("original_filename")
|
|
path String
|
|
durationSeconds Int? @map("duration_seconds")
|
|
quality String?
|
|
orientation String?
|
|
fileSize BigInt? @map("file_size")
|
|
thumbnailPath String? @map("thumbnail_path")
|
|
status UserUploadStatus @default(pending)
|
|
reviewedBy String? @map("reviewed_by")
|
|
reviewedAt DateTime? @map("reviewed_at")
|
|
reviewNotes String? @map("review_notes")
|
|
publicMediaId Int? @map("public_media_id")
|
|
uploadInviteId Int? @map("upload_invite_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation("UserUploads", fields: [userId], references: [id])
|
|
reviewer User? @relation("UserUploadReviewer", fields: [reviewedBy], references: [id])
|
|
uploadInvite UploadInvite? @relation(fields: [uploadInviteId], references: [id])
|
|
suggestedTags UserUploadSuggestedTag[]
|
|
|
|
@@index([userId], map: "idx_user_uploads_user")
|
|
@@index([status], map: "idx_user_uploads_status")
|
|
@@index([createdAt], map: "idx_user_uploads_created")
|
|
@@index([uploadInviteId], map: "idx_user_uploads_invite")
|
|
@@map("user_uploads")
|
|
}
|
|
|
|
model UploadInvite {
|
|
id Int @id @default(autoincrement())
|
|
code String @unique
|
|
label String?
|
|
createdBy String @map("created_by")
|
|
status UploadInviteStatus @default(active)
|
|
maxUploads Int? @map("max_uploads")
|
|
uploadCount Int @default(0) @map("upload_count")
|
|
expiresAt DateTime? @map("expires_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
creator User @relation("UploadInviteCreator", fields: [createdBy], references: [id])
|
|
uploads UserUpload[]
|
|
|
|
@@index([code], map: "idx_upload_invites_code")
|
|
@@index([status], map: "idx_upload_invites_status")
|
|
@@index([createdBy], map: "idx_upload_invites_created_by")
|
|
@@map("upload_invites")
|
|
}
|
|
|
|
// ============================================================================
|
|
// TAG SYSTEM
|
|
// ============================================================================
|
|
|
|
model TagCategory {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
displayOrder Int @default(0) @map("display_order")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
tags Tag[]
|
|
|
|
@@index([displayOrder], map: "idx_tag_categories_display_order")
|
|
@@map("tag_categories")
|
|
}
|
|
|
|
model Tag {
|
|
id Int @id @default(autoincrement())
|
|
categoryId Int @map("category_id")
|
|
name String
|
|
displayOrder Int @default(0) @map("display_order")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
category TagCategory @relation(fields: [categoryId], references: [id])
|
|
publicMedia PublicMediaTag[]
|
|
userUploadSuggestions UserUploadSuggestedTag[]
|
|
userPreferences UserTagPreference[]
|
|
|
|
@@index([categoryId], map: "idx_tags_category")
|
|
@@index([displayOrder], map: "idx_tags_display_order")
|
|
@@index([categoryId, name], map: "idx_tags_unique_name")
|
|
@@map("tags")
|
|
}
|
|
|
|
model PublicMediaTag {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
tagId Int @map("tag_id")
|
|
addedAt DateTime @default(now()) @map("added_at")
|
|
|
|
// Relations
|
|
media Video @relation("PublicMediaTags", fields: [mediaId], references: [id])
|
|
tag Tag @relation(fields: [tagId], references: [id])
|
|
|
|
@@index([mediaId], map: "idx_public_media_tags_media")
|
|
@@index([tagId], map: "idx_public_media_tags_tag")
|
|
@@index([mediaId, tagId], map: "idx_public_media_tags_unique")
|
|
@@map("public_media_tags")
|
|
}
|
|
|
|
model UserUploadSuggestedTag {
|
|
id Int @id @default(autoincrement())
|
|
uploadId Int @map("upload_id")
|
|
tagId Int @map("tag_id")
|
|
suggestedAt DateTime @default(now()) @map("suggested_at")
|
|
|
|
// Relations
|
|
upload UserUpload @relation(fields: [uploadId], references: [id])
|
|
tag Tag @relation(fields: [tagId], references: [id])
|
|
|
|
@@index([uploadId], map: "idx_user_upload_suggested_tags_upload")
|
|
@@index([tagId], map: "idx_user_upload_suggested_tags_tag")
|
|
@@index([uploadId, tagId], map: "idx_user_upload_suggested_tags_unique")
|
|
@@map("user_upload_suggested_tags")
|
|
}
|
|
|
|
model UserTagPreference {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
tagId Int @map("tag_id")
|
|
savedAt DateTime @default(now()) @map("saved_at")
|
|
|
|
// Relations
|
|
user User @relation("UserTagPreferences", fields: [userId], references: [id])
|
|
tag Tag @relation(fields: [tagId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_tag_preferences_user")
|
|
@@index([tagId], map: "idx_user_tag_preferences_tag")
|
|
@@index([userId, tagId], map: "idx_user_tag_preferences_unique")
|
|
@@map("user_tag_preferences")
|
|
}
|
|
|
|
model PublicMediaPerformer {
|
|
id Int @id @default(autoincrement())
|
|
mediaId Int @map("media_id")
|
|
performerId Int @map("performer_id")
|
|
addedAt DateTime @default(now()) @map("added_at")
|
|
|
|
// Relations
|
|
media Video @relation("PublicMediaPerformers", fields: [mediaId], references: [id])
|
|
performer Creator @relation(fields: [performerId], references: [id])
|
|
|
|
@@unique([mediaId, performerId], map: "idx_public_media_performers_unique")
|
|
@@index([mediaId], map: "idx_public_media_performers_media")
|
|
@@index([performerId], map: "idx_public_media_performers_performer")
|
|
@@map("public_media_performers")
|
|
}
|
|
|
|
// ============================================================================
|
|
// VIDEO DIGEST SYSTEM
|
|
// ============================================================================
|
|
|
|
model VideoDigest {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
jobId Int? @unique @map("job_id")
|
|
status DigestStatus @default(pending)
|
|
progress Int? @default(0)
|
|
frameCount Int? @map("frame_count")
|
|
config Json?
|
|
frameAnalyses Json? @map("frame_analyses")
|
|
transcript Json?
|
|
tags Json?
|
|
suggestedClips Json? @map("suggested_clips")
|
|
adCutSpec Json? @map("ad_cut_spec")
|
|
stageResults Json? @map("stage_results")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
error String?
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
job Job? @relation(fields: [jobId], references: [id])
|
|
digestVideoTags DigestVideoTag[]
|
|
selectedClips DigestSelectedClip[]
|
|
generatedClips DigestGeneratedClip[]
|
|
outputFolders DigestOutputFolder[]
|
|
compilations DigestCompilation[]
|
|
generatedScenes DigestGeneratedScene[]
|
|
suggestedTags DigestSuggestedTag[]
|
|
|
|
@@index([videoId], map: "idx_video_digests_video")
|
|
@@index([status], map: "idx_video_digests_status")
|
|
@@index([createdAt], map: "idx_video_digests_created")
|
|
@@map("video_digests")
|
|
}
|
|
|
|
model DigestVideoTag {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
videoId Int @map("video_id")
|
|
category String
|
|
value String
|
|
confidence Int?
|
|
source String? @default("digest")
|
|
evidence Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([digestId], map: "idx_digest_video_tags_digest")
|
|
@@index([videoId], map: "idx_digest_video_tags_video")
|
|
@@index([category], map: "idx_digest_video_tags_category")
|
|
@@index([value], map: "idx_digest_video_tags_value")
|
|
@@index([category, value], map: "idx_digest_video_tags_cat_val")
|
|
@@map("digest_video_tags")
|
|
}
|
|
|
|
model DigestSelectedClip {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
videoId Int @map("video_id")
|
|
clipType ClipType @map("clip_type")
|
|
startTime Int @map("start_time")
|
|
endTime Int @map("end_time")
|
|
duration Int
|
|
reason String?
|
|
interestScore Int? @map("interest_score")
|
|
position String?
|
|
transcriptHint String? @map("transcript_hint")
|
|
tags Json?
|
|
source ClipSource @default(machine)
|
|
isIncluded Int @default(1) @map("is_included")
|
|
isHook Int @default(0) @map("is_hook")
|
|
sequenceOrder Int @default(0) @map("sequence_order")
|
|
hookSourceClipId Int? @map("hook_source_clip_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
generatedClips DigestGeneratedClip[]
|
|
|
|
@@index([digestId], map: "idx_digest_selected_clips_digest")
|
|
@@index([videoId], map: "idx_digest_selected_clips_video")
|
|
@@index([clipType], map: "idx_digest_selected_clips_type")
|
|
@@index([source], map: "idx_digest_selected_clips_source")
|
|
@@index([sequenceOrder], map: "idx_digest_selected_clips_sequence")
|
|
@@index([isHook], map: "idx_digest_selected_clips_is_hook")
|
|
@@index([hookSourceClipId], map: "idx_digest_selected_clips_hook_source")
|
|
@@map("digest_selected_clips")
|
|
}
|
|
|
|
model DigestGeneratedClip {
|
|
id Int @id @default(autoincrement())
|
|
selectedClipId Int @map("selected_clip_id")
|
|
digestId Int @map("digest_id")
|
|
folderId Int? @map("folder_id")
|
|
clipPath String? @map("clip_path")
|
|
gifPath String? @map("gif_path")
|
|
status ClipStatus @default(pending)
|
|
error String?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
publishedToPublicMediaId Int? @map("published_to_public_media_id")
|
|
publishedAt DateTime? @map("published_at")
|
|
|
|
// Relations
|
|
selectedClip DigestSelectedClip @relation(fields: [selectedClipId], references: [id])
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
|
|
|
|
@@index([selectedClipId], map: "idx_digest_generated_clips_selected")
|
|
@@index([digestId], map: "idx_digest_generated_clips_digest")
|
|
@@index([status], map: "idx_digest_generated_clips_status")
|
|
@@index([folderId], map: "idx_digest_generated_clips_folder")
|
|
@@map("digest_generated_clips")
|
|
}
|
|
|
|
model DigestCompilation {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
videoId Int @map("video_id")
|
|
folderId Int? @map("folder_id")
|
|
filename String
|
|
name String?
|
|
path String
|
|
durationSeconds Int? @map("duration_seconds")
|
|
orientation String?
|
|
status String @default("pending")
|
|
error String?
|
|
hasCaptions Int @default(0) @map("has_captions")
|
|
captionStyle Json? @map("caption_style")
|
|
closingAdPath String? @map("closing_ad_path")
|
|
closingAdDuration Int? @map("closing_ad_duration")
|
|
tags Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
|
|
|
|
@@index([digestId], map: "idx_digest_compilations_digest")
|
|
@@index([videoId], map: "idx_digest_compilations_video")
|
|
@@index([status], map: "idx_digest_compilations_status")
|
|
@@index([folderId], map: "idx_digest_compilations_folder")
|
|
@@map("digest_compilations")
|
|
}
|
|
|
|
model DigestOutputFolder {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
videoId Int @map("video_id")
|
|
folderPath String @map("folder_path")
|
|
folderName String @map("folder_name")
|
|
folderType String @default("clips") @map("folder_type")
|
|
clipCount Int? @default(0) @map("clip_count")
|
|
compilationCount Int? @default(0) @map("compilation_count")
|
|
totalSize Int? @map("total_size")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
generatedClips DigestGeneratedClip[]
|
|
generatedScenes DigestGeneratedScene[]
|
|
compilations DigestCompilation[]
|
|
|
|
@@index([digestId], map: "idx_digest_output_folders_digest")
|
|
@@index([videoId], map: "idx_digest_output_folders_video")
|
|
@@index([folderPath], map: "idx_digest_output_folders_path")
|
|
@@map("digest_output_folders")
|
|
}
|
|
|
|
model DigestGeneratedScene {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
videoId Int @map("video_id")
|
|
folderId Int? @map("folder_id")
|
|
sceneNumber Int @map("scene_number")
|
|
scenePath String? @map("scene_path")
|
|
startTime Float @map("start_time") @db.Real
|
|
endTime Float @map("end_time") @db.Real
|
|
duration Float @db.Real
|
|
tags Json?
|
|
dominantPosition String? @map("dominant_position")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
publishedToPublicMediaId Int? @map("published_to_public_media_id")
|
|
publishedAt DateTime? @map("published_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
folder DigestOutputFolder? @relation(fields: [folderId], references: [id])
|
|
|
|
@@index([digestId], map: "idx_digest_generated_scenes_digest")
|
|
@@index([videoId], map: "idx_digest_generated_scenes_video")
|
|
@@index([folderId], map: "idx_digest_generated_scenes_folder")
|
|
@@index([scenePath], map: "idx_digest_generated_scenes_path")
|
|
@@map("digest_generated_scenes")
|
|
}
|
|
|
|
model VideoSceneCut {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
cuts Json
|
|
sceneCount Int @map("scene_count")
|
|
duration Float @db.Real
|
|
detector String @default("content")
|
|
threshold Float @default(27.0) @db.Real
|
|
transnetCuts Json? @map("transnet_cuts")
|
|
pyscenedetectCuts Json? @map("pyscenedetect_cuts")
|
|
clipCuts Json? @map("clip_cuts")
|
|
mergedCuts Json? @map("merged_cuts")
|
|
analysisMetadata Json? @map("analysis_metadata")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_scene_cuts_video")
|
|
@@index([detector], map: "idx_video_scene_cuts_detector")
|
|
@@map("video_scene_cuts")
|
|
}
|
|
|
|
model VideoTagTimeline {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
category String
|
|
value String
|
|
startTime Float @map("start_time") @db.Real
|
|
endTime Float @map("end_time") @db.Real
|
|
confidence Int?
|
|
source String? @default("digest")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_tag_timeline_video")
|
|
@@index([category], map: "idx_video_tag_timeline_category")
|
|
@@index([value], map: "idx_video_tag_timeline_value")
|
|
@@index([category, value], map: "idx_video_tag_timeline_cat_val")
|
|
@@index([videoId, startTime], map: "idx_video_tag_timeline_time")
|
|
@@map("video_tag_timeline")
|
|
}
|
|
|
|
model VideoSegment {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
segmentType SegmentType @map("segment_type")
|
|
startTime Float @map("start_time") @db.Real
|
|
endTime Float @map("end_time") @db.Real
|
|
duration Float @db.Real
|
|
tags Json?
|
|
vocalCategory VocalCategory? @map("vocal_category")
|
|
transcript String?
|
|
dominantPosition String? @map("dominant_position")
|
|
interestScore Int? @map("interest_score")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_segments_video")
|
|
@@index([segmentType], map: "idx_video_segments_type")
|
|
@@index([vocalCategory], map: "idx_video_segments_vocal_category")
|
|
@@index([videoId, startTime], map: "idx_video_segments_time")
|
|
@@map("video_segments")
|
|
}
|
|
|
|
model VideoTag {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
category String
|
|
value String
|
|
confidence Int?
|
|
source String? @default("digest")
|
|
evidence Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_tags_video")
|
|
@@index([category], map: "idx_video_tags_category")
|
|
@@index([value], map: "idx_video_tags_value")
|
|
@@index([category, value], map: "idx_video_tags_cat_val")
|
|
@@map("video_tags")
|
|
}
|
|
|
|
model DigestSuggestedTag {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
category String
|
|
value String
|
|
confidence Int?
|
|
evidence Json?
|
|
status SuggestedTagStatus @default(pending)
|
|
mappedTagId Int? @map("mapped_tag_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
reviewedAt DateTime? @map("reviewed_at")
|
|
|
|
// Relations
|
|
digest VideoDigest @relation(fields: [digestId], references: [id])
|
|
|
|
@@index([digestId], map: "idx_digest_suggested_tags_digest")
|
|
@@index([status], map: "idx_digest_suggested_tags_status")
|
|
@@index([category], map: "idx_digest_suggested_tags_category")
|
|
@@index([mappedTagId], map: "idx_digest_suggested_tags_mapped")
|
|
@@map("digest_suggested_tags")
|
|
}
|
|
|
|
model DigestClipTag {
|
|
id Int @id @default(autoincrement())
|
|
digestId Int @map("digest_id")
|
|
clipId Int @map("clip_id")
|
|
videoId Int @map("video_id")
|
|
category String
|
|
value String
|
|
confidence Int?
|
|
source String? @default("clip_analysis")
|
|
evidence Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([digestId], map: "idx_digest_clip_tags_digest")
|
|
@@index([clipId], map: "idx_digest_clip_tags_clip")
|
|
@@index([videoId], map: "idx_digest_clip_tags_video")
|
|
@@index([category], map: "idx_digest_clip_tags_category")
|
|
@@index([value], map: "idx_digest_clip_tags_value")
|
|
@@map("digest_clip_tags")
|
|
}
|
|
|
|
// ============================================================================
|
|
// WATCH PARTY SYSTEM
|
|
// ============================================================================
|
|
|
|
model WatchPartySession {
|
|
id Int @id @default(autoincrement())
|
|
hostUserId String @map("host_user_id")
|
|
mediaId Int @map("media_id")
|
|
status WatchPartyStatus @default(active)
|
|
inviteCode String @unique @map("invite_code")
|
|
currentTime Int @default(0) @map("current_time")
|
|
isPlaying Boolean @default(false) @map("is_playing")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
endedAt DateTime? @map("ended_at")
|
|
|
|
// Relations
|
|
host User @relation("WatchPartyHost", fields: [hostUserId], references: [id])
|
|
participants WatchPartyParticipant[]
|
|
messages WatchPartyChatMessage[]
|
|
reactions WatchPartyReaction[]
|
|
invites WatchPartyInvite[]
|
|
|
|
@@index([hostUserId], map: "idx_watch_party_sessions_host")
|
|
@@index([mediaId], map: "idx_watch_party_sessions_media")
|
|
@@index([status], map: "idx_watch_party_sessions_status")
|
|
@@index([inviteCode], map: "idx_watch_party_sessions_invite")
|
|
@@map("watch_party_sessions")
|
|
}
|
|
|
|
model WatchPartyParticipant {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int @map("session_id")
|
|
userId String @map("user_id")
|
|
joinedAt DateTime @default(now()) @map("joined_at")
|
|
leftAt DateTime? @map("left_at")
|
|
|
|
// Relations
|
|
session WatchPartySession @relation(fields: [sessionId], references: [id])
|
|
user User @relation("WatchPartyParticipant", fields: [userId], references: [id])
|
|
|
|
@@index([sessionId], map: "idx_watch_party_participants_session")
|
|
@@index([userId], map: "idx_watch_party_participants_user")
|
|
@@map("watch_party_participants")
|
|
}
|
|
|
|
model WatchPartyChatMessage {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int @map("session_id")
|
|
userId String @map("user_id")
|
|
commentId Int @map("comment_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
session WatchPartySession @relation(fields: [sessionId], references: [id])
|
|
user User @relation("WatchPartyChatUser", fields: [userId], references: [id])
|
|
comment Comment @relation(fields: [commentId], references: [id])
|
|
|
|
@@index([sessionId], map: "idx_watch_party_chat_session")
|
|
@@index([userId], map: "idx_watch_party_chat_user")
|
|
@@map("watch_party_chat_messages")
|
|
}
|
|
|
|
model WatchPartyReaction {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int @map("session_id")
|
|
userId String @map("user_id")
|
|
reactionType ReactionType @map("reaction_type")
|
|
videoTimestamp Int @map("video_timestamp")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
session WatchPartySession @relation(fields: [sessionId], references: [id])
|
|
user User @relation("WatchPartyReactionUser", fields: [userId], references: [id])
|
|
|
|
@@index([sessionId], map: "idx_watch_party_reactions_session")
|
|
@@index([userId], map: "idx_watch_party_reactions_user")
|
|
@@map("watch_party_reactions")
|
|
}
|
|
|
|
model WatchPartyInvite {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int @map("session_id")
|
|
userId String @map("user_id")
|
|
invitedBy String @map("invited_by")
|
|
status String @default("pending")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
acceptedAt DateTime? @map("accepted_at")
|
|
|
|
// Relations
|
|
session WatchPartySession @relation(fields: [sessionId], references: [id])
|
|
user User @relation("WatchPartyInvitee", fields: [userId], references: [id])
|
|
inviter User @relation("WatchPartyInviter", fields: [invitedBy], references: [id])
|
|
|
|
@@index([sessionId], map: "idx_watch_party_invites_session")
|
|
@@index([userId], map: "idx_watch_party_invites_user")
|
|
@@map("watch_party_invites")
|
|
}
|
|
|
|
// ============================================================================
|
|
// TAG GENERATION JOBS
|
|
// ============================================================================
|
|
|
|
model TagGenerationJob {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
jobId Int? @map("job_id")
|
|
status String @default("pending")
|
|
progress Int? @default(0)
|
|
generatedTags Json? @map("generated_tags")
|
|
rawResponse String? @map("raw_response")
|
|
error String?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
job Job? @relation(fields: [jobId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_tag_generation_jobs_video")
|
|
@@index([status], map: "idx_tag_generation_jobs_status")
|
|
@@map("tag_generation_jobs")
|
|
}
|
|
|
|
// ============================================================================
|
|
// CREATORS & PERFORMERS
|
|
// ============================================================================
|
|
|
|
model Creator {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
stage_name String? @map("stage_name")
|
|
performerGender String? @map("performer_gender")
|
|
faceEmbedding Json? @map("face_embedding")
|
|
referenceImagePath String? @map("reference_image_path")
|
|
performerCategory String? @map("performer_category")
|
|
status String @default("active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
publicMediaPerformers PublicMediaPerformer[]
|
|
performerFaces PerformerFace[]
|
|
performerDiscrepancies PerformerDiscrepancy[]
|
|
|
|
@@index([name], map: "idx_creators_name")
|
|
@@index([performerCategory], map: "idx_creators_category")
|
|
@@map("creators")
|
|
}
|
|
|
|
model PerformerFace {
|
|
id Int @id @default(autoincrement())
|
|
performerId Int @map("performer_id")
|
|
sourcePath String @map("source_path")
|
|
frameTimestamp Float? @map("frame_timestamp") @db.Real
|
|
bbox Json
|
|
embedding Json
|
|
gender Int
|
|
age Int
|
|
detScore Float @map("det_score") @db.Real
|
|
verifiedByUser Boolean @default(false) @map("verified_by_user")
|
|
confidence Float? @db.Real
|
|
isReferenceImage Boolean @default(false) @map("is_reference_image")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
performer Creator @relation(fields: [performerId], references: [id])
|
|
|
|
@@index([performerId], map: "idx_performer_faces_performer")
|
|
@@index([sourcePath], map: "idx_performer_faces_source")
|
|
@@index([isReferenceImage], map: "idx_performer_faces_reference")
|
|
@@map("performer_faces")
|
|
}
|
|
|
|
model PerformerDiscrepancy {
|
|
id Int @id @default(autoincrement())
|
|
videoPath String @map("video_path")
|
|
frameTimestamp Float @map("frame_timestamp") @db.Real
|
|
assignedPerformerId Int @map("assigned_performer_id")
|
|
detectedPerformerId Int? @map("detected_performer_id")
|
|
similarity Float @map("similarity") @db.Real
|
|
faceData Json @map("face_data")
|
|
resolutionStatus String @default("pending") @map("resolution_status")
|
|
resolutionAction String? @map("resolution_action")
|
|
resolutionNotes String? @map("resolution_notes")
|
|
resolvedBy String? @map("resolved_by")
|
|
resolvedAt DateTime? @map("resolved_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
assignedPerformer Creator @relation(fields: [assignedPerformerId], references: [id])
|
|
resolver User? @relation("PerformerDiscrepancyResolver", fields: [resolvedBy], references: [id])
|
|
|
|
@@index([assignedPerformerId], map: "idx_performer_discrepancies_assigned")
|
|
@@index([detectedPerformerId], map: "idx_performer_discrepancies_detected")
|
|
@@index([resolutionStatus], map: "idx_performer_discrepancies_status")
|
|
@@map("performer_discrepancies")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PIPELINE SYSTEM
|
|
// ============================================================================
|
|
|
|
model Pipeline {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
status PipelineStatus @default(draft)
|
|
config Json?
|
|
templateId Int? @map("template_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
error String?
|
|
|
|
// Relations
|
|
template PipelineTemplate? @relation(fields: [templateId], references: [id])
|
|
steps PipelineStep[]
|
|
jobs Job[]
|
|
|
|
@@index([status], map: "idx_pipelines_status")
|
|
@@index([templateId], map: "idx_pipelines_template")
|
|
@@map("pipelines")
|
|
}
|
|
|
|
model PipelineStep {
|
|
id Int @id @default(autoincrement())
|
|
pipelineId Int @map("pipeline_id")
|
|
stepName String @map("step_name")
|
|
stepType String @map("step_type")
|
|
sequenceOrder Int @map("sequence_order")
|
|
status PipelineStepStatus @default(pending)
|
|
config Json?
|
|
inputs Json?
|
|
outputs Json?
|
|
dependsOn Json? @map("depends_on")
|
|
error String?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
// Relations
|
|
pipeline Pipeline @relation(fields: [pipelineId], references: [id])
|
|
jobs Job[]
|
|
events PipelineStepEvent[]
|
|
|
|
@@index([pipelineId], map: "idx_pipeline_steps_pipeline")
|
|
@@index([status], map: "idx_pipeline_steps_status")
|
|
@@index([sequenceOrder], map: "idx_pipeline_steps_sequence")
|
|
@@map("pipeline_steps")
|
|
}
|
|
|
|
model PipelineStepEvent {
|
|
id Int @id @default(autoincrement())
|
|
stepId Int @map("step_id")
|
|
eventType String @map("event_type")
|
|
message String?
|
|
data Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
step PipelineStep @relation(fields: [stepId], references: [id])
|
|
|
|
@@index([stepId], map: "idx_pipeline_step_events_step")
|
|
@@index([eventType], map: "idx_pipeline_step_events_type")
|
|
@@map("pipeline_step_events")
|
|
}
|
|
|
|
model ResourceSnapshot {
|
|
id Int @id @default(autoincrement())
|
|
timestamp DateTime @default(now())
|
|
cpuPercent Float? @map("cpu_percent") @db.Real
|
|
memoryMb Int? @map("memory_mb")
|
|
gpuPercent Float? @map("gpu_percent") @db.Real
|
|
vramMb Int? @map("vram_mb")
|
|
diskUsageMb Int? @map("disk_usage_mb")
|
|
activeJobs Int? @map("active_jobs")
|
|
queuedJobs Int? @map("queued_jobs")
|
|
metadata Json?
|
|
|
|
@@index([timestamp], map: "idx_resource_snapshots_timestamp")
|
|
@@map("resource_snapshots")
|
|
}
|
|
|
|
model PipelineTemplate {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
category String?
|
|
defaultConfig Json? @map("default_config")
|
|
stepDefinitions Json @map("step_definitions")
|
|
isPublic Boolean @default(false) @map("is_public")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
pipelines Pipeline[]
|
|
|
|
@@index([category], map: "idx_pipeline_templates_category")
|
|
@@index([isPublic], map: "idx_pipeline_templates_public")
|
|
@@map("pipeline_templates")
|
|
}
|
|
|
|
// ============================================================================
|
|
// SUBSCRIPTION & PAYMENTS
|
|
// ============================================================================
|
|
|
|
model SubscriptionPlan {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
priceCAD Int @map("price_cad")
|
|
durationDays Int @map("duration_days")
|
|
features Json?
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Stripe integration
|
|
stripeProductId String? @map("stripe_product_id")
|
|
stripePriceId String? @map("stripe_price_id")
|
|
stripeYearlyPriceId String? @map("stripe_yearly_price_id")
|
|
yearlyPriceCAD Int? @map("yearly_price_cad")
|
|
description String? @db.Text
|
|
tier Int @default(0)
|
|
displayOrder Int @default(0) @map("display_order")
|
|
|
|
// Page content fields
|
|
slug String? @unique
|
|
coverPhoto String? @map("cover_photo")
|
|
coverVideoId Int? @map("cover_video_id")
|
|
richDescription String? @db.Text @map("rich_description")
|
|
ctaText String? @map("cta_text")
|
|
ctaSubtext String? @map("cta_subtext")
|
|
highlightPlan Boolean @default(false) @map("highlight_plan")
|
|
|
|
// Relations
|
|
subscriptions UserSubscription[]
|
|
|
|
@@map("subscription_plans")
|
|
}
|
|
|
|
model UserSubscription {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
planId Int @map("plan_id")
|
|
status SubscriptionStatus @default(active)
|
|
startDate DateTime @map("start_date")
|
|
endDate DateTime @map("end_date")
|
|
cancelledAt DateTime? @map("cancelled_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Stripe integration
|
|
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
|
|
stripeCustomerId String? @map("stripe_customer_id")
|
|
currentPeriodEnd DateTime? @map("current_period_end")
|
|
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
|
|
|
// Relations
|
|
user User @relation("UserSubscriptions", fields: [userId], references: [id])
|
|
plan SubscriptionPlan @relation(fields: [planId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_subscriptions_user")
|
|
@@index([planId], map: "idx_user_subscriptions_plan")
|
|
@@index([status], map: "idx_user_subscriptions_status")
|
|
@@map("user_subscriptions")
|
|
}
|
|
|
|
model Invoice {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
subscriptionId Int? @map("subscription_id")
|
|
amountCAD Int @map("amount_cad")
|
|
status InvoiceStatus @default(pending)
|
|
issuedAt DateTime @default(now()) @map("issued_at")
|
|
paidAt DateTime? @map("paid_at")
|
|
dueDate DateTime @map("due_date")
|
|
description String?
|
|
metadata Json?
|
|
|
|
// Stripe integration
|
|
stripeInvoiceId String? @unique @map("stripe_invoice_id")
|
|
type String @default("subscription") @map("invoice_type") // subscription|product|donation
|
|
|
|
// Relations
|
|
user User @relation("UserInvoices", fields: [userId], references: [id])
|
|
payments Payment[]
|
|
|
|
@@index([userId], map: "idx_invoices_user")
|
|
@@index([status], map: "idx_invoices_status")
|
|
@@index([issuedAt], map: "idx_invoices_issued")
|
|
@@map("invoices")
|
|
}
|
|
|
|
model Payment {
|
|
id Int @id @default(autoincrement())
|
|
invoiceId Int @map("invoice_id")
|
|
userId String @map("user_id")
|
|
amountCAD Int @map("amount_cad")
|
|
method PaymentMethod
|
|
status PaymentStatus @default(pending)
|
|
externalId String? @map("external_id")
|
|
metadata Json?
|
|
processedAt DateTime? @map("processed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Stripe integration
|
|
stripePaymentIntentId String? @unique @map("stripe_payment_intent_id")
|
|
stripeCheckoutSessionId String? @map("stripe_checkout_session_id")
|
|
|
|
// Relations
|
|
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
|
user User @relation("UserPayments", fields: [userId], references: [id])
|
|
auditLogs PaymentAuditLog[]
|
|
|
|
@@index([invoiceId], map: "idx_payments_invoice")
|
|
@@index([userId], map: "idx_payments_user")
|
|
@@index([status], map: "idx_payments_status")
|
|
@@map("payments")
|
|
}
|
|
|
|
model PaymentAuditLog {
|
|
id Int @id @default(autoincrement())
|
|
paymentId Int @map("payment_id")
|
|
action String
|
|
oldStatus String? @map("old_status")
|
|
newStatus String? @map("new_status")
|
|
userId String? @map("user_id")
|
|
metadata Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
payment Payment @relation(fields: [paymentId], references: [id])
|
|
user User? @relation("PaymentAuditUser", fields: [userId], references: [id])
|
|
|
|
@@index([paymentId], map: "idx_payment_audit_log_payment")
|
|
@@index([action], map: "idx_payment_audit_log_action")
|
|
@@index([createdAt], map: "idx_payment_audit_log_created")
|
|
@@map("payment_audit_log")
|
|
}
|
|
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
title String
|
|
description String? @db.Text
|
|
priceCAD Int @map("price_cad")
|
|
type ProductType
|
|
stripeProductId String? @map("stripe_product_id")
|
|
stripePriceId String? @map("stripe_price_id")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
imageUrl String? @map("image_url")
|
|
photoId Int? @map("photo_id")
|
|
videoId Int? @map("video_id")
|
|
galleryPhotoIds Json? @map("gallery_photo_ids")
|
|
downloadUrl String? @map("download_url")
|
|
metadata Json?
|
|
maxPurchases Int? @map("max_purchases")
|
|
purchaseCount Int @default(0) @map("purchase_count")
|
|
createdByUserId String? @map("created_by_user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
orders Order[]
|
|
galleryAd Ad?
|
|
|
|
@@index([type], map: "idx_products_type")
|
|
@@index([isActive], map: "idx_products_active")
|
|
@@map("products")
|
|
}
|
|
|
|
model Order {
|
|
id String @id @default(cuid())
|
|
userId String? @map("user_id")
|
|
productId String? @map("product_id")
|
|
amountCAD Int @map("amount_cad")
|
|
status OrderStatus @default(PENDING)
|
|
stripeCheckoutSessionId String? @unique @map("stripe_checkout_session_id")
|
|
stripePaymentIntentId String? @map("stripe_payment_intent_id")
|
|
type String @default("product") @map("order_type") // product|donation
|
|
|
|
// Buyer info (for guests)
|
|
buyerEmail String @map("buyer_email")
|
|
buyerName String? @map("buyer_name")
|
|
|
|
// Donation-specific
|
|
donorMessage String? @db.Text @map("donor_message")
|
|
isAnonymous Boolean @default(false) @map("is_anonymous")
|
|
|
|
completedAt DateTime? @map("completed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
user User? @relation("UserOrders", fields: [userId], references: [id])
|
|
product Product? @relation(fields: [productId], references: [id])
|
|
donationPageId String? @map("donation_page_id")
|
|
donationPage DonationPage? @relation("DonationPageOrders", fields: [donationPageId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId], map: "idx_orders_user")
|
|
@@index([productId], map: "idx_orders_product")
|
|
@@index([status], map: "idx_orders_status")
|
|
@@index([type], map: "idx_orders_type")
|
|
@@index([donationPageId], map: "idx_orders_donation_page")
|
|
@@map("orders")
|
|
}
|
|
|
|
enum DonationPageStatus {
|
|
DRAFT
|
|
ACTIVE
|
|
PAUSED
|
|
ARCHIVED
|
|
}
|
|
|
|
model DonationPage {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
title String
|
|
description String? @db.Text
|
|
status DonationPageStatus @default(DRAFT)
|
|
|
|
// Donation config (per-page, mirrors PaymentSettings fields)
|
|
suggestedAmounts Json @default("[1000, 2500, 5000, 10000]")
|
|
minimumAmount Int @default(500) @map("minimum_amount")
|
|
thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message")
|
|
|
|
// Media
|
|
coverPhoto String? @map("cover_photo")
|
|
coverVideoId Int? @map("cover_video_id")
|
|
|
|
// Display options
|
|
highlightPage Boolean @default(false) @map("highlight_page")
|
|
showDonorCount Boolean @default(true) @map("show_donor_count")
|
|
showTotalRaised Boolean @default(false) @map("show_total_raised")
|
|
goalAmount Int? @map("goal_amount")
|
|
|
|
// Creator tracking
|
|
createdByUserId String? @map("created_by_user_id")
|
|
createdByUser User? @relation("DonationPageCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
orders Order[] @relation("DonationPageOrders")
|
|
|
|
@@index([status])
|
|
@@map("donation_pages")
|
|
}
|
|
|
|
model PaymentSettings {
|
|
id String @id @default(cuid())
|
|
stripeSecretKey String @default("") @map("stripe_secret_key")
|
|
stripePublishableKey String @default("") @map("stripe_publishable_key")
|
|
stripeWebhookSecret String @default("") @map("stripe_webhook_secret")
|
|
defaultCurrency String @default("cad") @map("default_currency")
|
|
|
|
// Donation settings
|
|
enableDonations Boolean @default(true) @map("enable_donations")
|
|
donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts")
|
|
donationMinimum Int @default(500) @map("donation_minimum")
|
|
donationPageTitle String @default("Support Our Work") @map("donation_page_title")
|
|
donationPageDescription String? @db.Text @map("donation_page_description")
|
|
thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message")
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("payment_settings")
|
|
}
|
|
|
|
// ============================================================================
|
|
// NOTIFICATIONS
|
|
// ============================================================================
|
|
|
|
model Notification {
|
|
id Int @id @default(autoincrement())
|
|
userId String @map("user_id")
|
|
type NotificationType
|
|
title String
|
|
message String
|
|
metadata Json?
|
|
isRead Boolean @default(false) @map("is_read")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
readAt DateTime? @map("read_at")
|
|
|
|
// Relations
|
|
user User @relation("UserNotifications", fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_notifications_user")
|
|
@@index([isRead], map: "idx_notifications_read")
|
|
@@index([type], map: "idx_notifications_type")
|
|
@@index([createdAt], map: "idx_notifications_created")
|
|
@@map("notifications")
|
|
}
|
|
|
|
model NotificationPreferences {
|
|
id Int @id @default(autoincrement())
|
|
userId String @unique @map("user_id")
|
|
enableFriendRequests Boolean @default(true) @map("enable_friend_requests")
|
|
enableComments Boolean @default(true) @map("enable_comments")
|
|
enableUploadApprovals Boolean @default(true) @map("enable_upload_approvals")
|
|
enableAchievements Boolean @default(true) @map("enable_achievements")
|
|
enableSystemUpdates Boolean @default(true) @map("enable_system_updates")
|
|
emailNotifications Boolean @default(false) @map("email_notifications")
|
|
digestFrequency String @default("none") @map("digest_frequency") // "none" | "daily" | "weekly"
|
|
lastDigestSentAt DateTime? @map("last_digest_sent_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
// Relations
|
|
user User @relation("NotificationPreferences", fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_notification_preferences_user")
|
|
@@map("notification_preferences")
|
|
}
|
|
|
|
// ============================================================================
|
|
// GEO-BLOCKING
|
|
// ============================================================================
|
|
|
|
model GeoBlockingRule {
|
|
id Int @id @default(autoincrement())
|
|
countryCode String @map("country_code")
|
|
action String // 'block' | 'allow'
|
|
reason String?
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime? @map("updated_at")
|
|
|
|
@@index([countryCode], map: "idx_geo_blocking_country")
|
|
@@index([isActive], map: "idx_geo_blocking_active")
|
|
@@map("geo_blocking_rules")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PUBLISHED INBOX FILES
|
|
// ============================================================================
|
|
|
|
model PublishedInboxFile {
|
|
id Int @id @default(autoincrement())
|
|
originalInboxPath String @map("original_inbox_path")
|
|
publishedToPublicMediaId Int @map("published_to_public_media_id")
|
|
publishedAt DateTime @map("published_at")
|
|
movedToPath String? @map("moved_to_path")
|
|
metadataSnapshot Json? @map("metadata_snapshot")
|
|
|
|
@@index([originalInboxPath], map: "idx_published_inbox_files_inbox_path")
|
|
@@index([publishedToPublicMediaId], map: "idx_published_inbox_files_public_media")
|
|
@@map("published_inbox_files")
|
|
}
|
|
|
|
// ============================================================================
|
|
// VIDEO OCR RESULTS
|
|
// ============================================================================
|
|
|
|
model VideoOcrResult {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
fullText String @map("full_text")
|
|
structuredData Json? @map("structured_data")
|
|
confidence Float? @db.Real
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_ocr_results_video")
|
|
@@map("video_ocr_results")
|
|
}
|
|
|
|
// Enhanced video analytics models
|
|
model VideoView {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
userId String? @map("user_id")
|
|
ipAddressHash String? @map("ip_address_hash") @db.VarChar(64) // SHA-256 hash
|
|
userAgentHash String? @map("user_agent_hash") @db.VarChar(64) // SHA-256 hash
|
|
referer String? @db.Text
|
|
watchTimeSeconds Int @default(0) @map("watch_time_seconds")
|
|
completed Boolean @default(false)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
|
user User? @relation("VideoViews", fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([videoId], map: "idx_video_views_video")
|
|
@@index([userId], map: "idx_video_views_user")
|
|
@@index([createdAt], map: "idx_video_views_created")
|
|
@@index([videoId, createdAt], map: "idx_video_views_video_created")
|
|
@@map("video_views")
|
|
}
|
|
|
|
model VideoEvent {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
viewId Int? @map("view_id")
|
|
eventType String @map("event_type") @db.VarChar(50) // play, pause, seek, complete
|
|
timestamp Decimal @db.Decimal(10, 2) // Video timestamp in seconds
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([videoId], map: "idx_video_events_video")
|
|
@@index([viewId], map: "idx_video_events_view")
|
|
@@index([eventType], map: "idx_video_events_type")
|
|
@@index([createdAt], map: "idx_video_events_created")
|
|
@@map("video_events")
|
|
}
|
|
|
|
model VideoScheduleHistory {
|
|
id Int @id @default(autoincrement())
|
|
videoId Int @map("video_id")
|
|
action String @db.VarChar(20) // 'publish' or 'unpublish'
|
|
scheduledFor DateTime @map("scheduled_for")
|
|
executedAt DateTime? @map("executed_at")
|
|
status String @db.VarChar(20) // 'pending', 'completed', 'failed', 'cancelled'
|
|
error String? @db.Text
|
|
scheduledByUserId String @map("scheduled_by_user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
|
scheduledBy User @relation("VideoScheduleHistory", fields: [scheduledByUserId], references: [id])
|
|
|
|
@@index([videoId], map: "idx_video_schedule_history_video")
|
|
@@index([scheduledFor], map: "idx_video_schedule_history_scheduled")
|
|
@@index([status], map: "idx_video_schedule_history_status")
|
|
@@index([scheduledByUserId], map: "idx_video_schedule_history_user")
|
|
@@map("video_schedule_history")
|
|
}
|
|
|
|
// ============================================================================
|
|
// DOCS ANALYTICS
|
|
// ============================================================================
|
|
|
|
model DocsPageView {
|
|
id String @id @default(cuid())
|
|
path String // e.g. "/docs/getting-started/"
|
|
referrer String? @db.Text // document.referrer
|
|
sessionHash String? // anonymous session UUID (sessionStorage)
|
|
userAgent String? // for device type breakdown
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
@@index([path, createdAt])
|
|
@@map("docs_page_views")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PHOTO GALLERY
|
|
// ============================================================================
|
|
|
|
model Photo {
|
|
id Int @id @default(autoincrement())
|
|
path String @unique // Full path to original file
|
|
filename String // UUID filename on disk
|
|
originalFilename String? @map("original_filename") // Original upload filename
|
|
title String?
|
|
description String? @db.Text
|
|
producer String?
|
|
creator String?
|
|
tags Json? // String array
|
|
|
|
// Image metadata (from sharp)
|
|
width Int?
|
|
height Int?
|
|
orientation String? // H / V / S (horizontal/vertical/square)
|
|
fileSize BigInt? @map("file_size")
|
|
format String? // jpeg, png, webp, avif, gif, tiff, heic
|
|
colorSpace String? @map("color_space") // srgb, display-p3, etc.
|
|
hasAlpha Boolean? @default(false) @map("has_alpha")
|
|
dpi Int?
|
|
|
|
// EXIF data
|
|
cameraMake String? @map("camera_make")
|
|
cameraModel String? @map("camera_model")
|
|
focalLength String? @map("focal_length")
|
|
aperture String?
|
|
shutterSpeed String? @map("shutter_speed")
|
|
iso Int?
|
|
takenAt DateTime? @map("taken_at")
|
|
gpsLatitude Float? @map("gps_latitude") @db.Real
|
|
gpsLongitude Float? @map("gps_longitude") @db.Real
|
|
|
|
// Processed variants
|
|
thumbnailPath String? @map("thumbnail_path")
|
|
mediumPath String? @map("medium_path")
|
|
largePath String? @map("large_path")
|
|
webpPath String? @map("webp_path")
|
|
|
|
// Publishing (mirrors Video)
|
|
isPublished Boolean @default(false) @map("is_published")
|
|
publishedAt DateTime? @map("published_at")
|
|
category String?
|
|
accessLevel String @default("free") @map("access_level")
|
|
position Int? @default(0)
|
|
isLocked Boolean @default(false) @map("is_locked")
|
|
scheduledPublishAt DateTime? @map("scheduled_publish_at")
|
|
scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at")
|
|
|
|
// Engagement counters
|
|
viewCount Int @default(0) @map("view_count")
|
|
upvoteCount Int @default(0) @map("upvote_count")
|
|
commentCount Int @default(0) @map("comment_count")
|
|
|
|
// Album membership
|
|
albumId Int? @map("album_id")
|
|
albumPosition Int? @default(0) @map("album_position")
|
|
|
|
// Tracking
|
|
uploaderId String? @map("uploader_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
album PhotoAlbum? @relation("AlbumPhotos", fields: [albumId], references: [id], onDelete: SetNull)
|
|
uploader User? @relation("PhotoUploader", fields: [uploaderId], references: [id])
|
|
upvotes PhotoUpvote[]
|
|
comments PhotoComment[]
|
|
views PhotoView[]
|
|
reactions PhotoReaction[]
|
|
coverForAlbum PhotoAlbum? @relation("AlbumCover")
|
|
|
|
@@index([orientation], map: "idx_photos_orientation")
|
|
@@index([producer], map: "idx_photos_producer")
|
|
@@index([isPublished, isLocked], map: "idx_photos_published_locked")
|
|
@@index([category, isPublished], map: "idx_photos_category_published")
|
|
@@index([albumId, albumPosition], map: "idx_photos_album_position")
|
|
@@index([createdAt], map: "idx_photos_created_at")
|
|
@@index([uploaderId], map: "idx_photos_uploader")
|
|
@@map("photos")
|
|
}
|
|
|
|
model PhotoAlbum {
|
|
id Int @id @default(autoincrement())
|
|
title String
|
|
description String? @db.Text
|
|
coverPhotoId Int? @unique @map("cover_photo_id")
|
|
|
|
// Publishing
|
|
isPublished Boolean @default(false) @map("is_published")
|
|
publishedAt DateTime? @map("published_at")
|
|
category String?
|
|
accessLevel String @default("free") @map("access_level")
|
|
position Int? @default(0)
|
|
isLocked Boolean @default(false) @map("is_locked")
|
|
|
|
// Aggregate counters
|
|
viewCount Int @default(0) @map("view_count")
|
|
upvoteCount Int @default(0) @map("upvote_count")
|
|
photoCount Int @default(0) @map("photo_count")
|
|
|
|
// Scheduling
|
|
scheduledPublishAt DateTime? @map("scheduled_publish_at")
|
|
scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at")
|
|
|
|
// Tracking
|
|
creatorId String? @map("creator_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
photos Photo[] @relation("AlbumPhotos")
|
|
coverPhoto Photo? @relation("AlbumCover", fields: [coverPhotoId], references: [id], onDelete: SetNull)
|
|
creator User? @relation("AlbumCreator", fields: [creatorId], references: [id])
|
|
|
|
@@index([isPublished], map: "idx_photo_albums_published")
|
|
@@index([creatorId], map: "idx_photo_albums_creator")
|
|
@@map("photo_albums")
|
|
}
|
|
|
|
model PhotoUpvote {
|
|
id Int @id @default(autoincrement())
|
|
photoId Int @map("photo_id")
|
|
sessionId String @map("session_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade)
|
|
session Session @relation("SessionPhotoUpvotes", fields: [sessionId], references: [id])
|
|
|
|
@@unique([photoId, sessionId], map: "idx_photo_upvotes_unique")
|
|
@@index([photoId], map: "idx_photo_upvotes_photo")
|
|
@@map("photo_upvotes")
|
|
}
|
|
|
|
model PhotoComment {
|
|
id Int @id @default(autoincrement())
|
|
photoId Int @map("photo_id")
|
|
sessionId String @map("session_id")
|
|
userId String? @map("user_id")
|
|
content String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
safetyStatus String @default("approved") @map("safety_status")
|
|
isHidden Boolean @default(false) @map("is_hidden")
|
|
|
|
photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade)
|
|
session Session @relation("SessionPhotoComments", fields: [sessionId], references: [id])
|
|
user User? @relation("PhotoCommentUser", fields: [userId], references: [id])
|
|
|
|
@@index([photoId, createdAt], map: "idx_photo_comments_photo_date")
|
|
@@index([sessionId], map: "idx_photo_comments_session")
|
|
@@map("photo_comments")
|
|
}
|
|
|
|
model PhotoView {
|
|
id Int @id @default(autoincrement())
|
|
photoId Int @map("photo_id")
|
|
sessionId String? @map("session_id")
|
|
userId String? @map("user_id")
|
|
ipAddressHash String? @map("ip_address_hash")
|
|
viewedAt DateTime @default(now()) @map("viewed_at")
|
|
|
|
photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([photoId, viewedAt], map: "idx_photo_views_photo_date")
|
|
@@index([sessionId], map: "idx_photo_views_session")
|
|
@@map("photo_views")
|
|
}
|
|
|
|
model PhotoReaction {
|
|
id Int @id @default(autoincrement())
|
|
photoId Int @map("photo_id")
|
|
sessionId String @map("session_id")
|
|
reactionType String @map("reaction_type") // like, love, laugh, wow, sad, angry
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade)
|
|
session Session @relation("SessionPhotoReactions", fields: [sessionId], references: [id])
|
|
|
|
@@unique([photoId, sessionId, reactionType], map: "idx_photo_reactions_unique")
|
|
@@index([photoId], map: "idx_photo_reactions_photo")
|
|
@@map("photo_reactions")
|
|
}
|
|
|
|
// ============================================================================
|
|
// DOCS COMMENTS (Gitea Issues-backed)
|
|
// ============================================================================
|
|
|
|
enum DocsCommentStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model DocsComment {
|
|
id String @id @default(cuid())
|
|
pagePath String
|
|
giteaIssueNumber Int
|
|
giteaCommentId BigInt
|
|
authorName String
|
|
authorEmail String?
|
|
status DocsCommentStatus @default(PENDING)
|
|
reviewedAt DateTime?
|
|
reviewedBy String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([pagePath, status])
|
|
@@index([status, createdAt])
|
|
@@index([giteaCommentId])
|
|
@@map("docs_comments")
|
|
}
|
|
|
|
// ============================================================================
|
|
// SMS CAMPAIGNS
|
|
// ============================================================================
|
|
|
|
enum SmsContactListStatus {
|
|
ACTIVE
|
|
ARCHIVED
|
|
}
|
|
|
|
enum SmsCampaignStatus {
|
|
DRAFT
|
|
RUNNING
|
|
PAUSED
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
enum SmsMessageDirection {
|
|
OUTBOUND
|
|
INBOUND
|
|
}
|
|
|
|
enum SmsMessageStatus {
|
|
PENDING
|
|
SENT
|
|
FAILED
|
|
DELIVERED
|
|
}
|
|
|
|
enum SmsResponseType {
|
|
POSITIVE
|
|
NEGATIVE
|
|
QUESTION
|
|
OPT_OUT
|
|
NEUTRAL
|
|
}
|
|
|
|
enum SmsConversationStatus {
|
|
ACTIVE
|
|
OPTED_OUT
|
|
CLOSED
|
|
}
|
|
|
|
model SmsContactList {
|
|
id String @id @default(cuid())
|
|
name String
|
|
originalFilename String?
|
|
totalContacts Int @default(0)
|
|
status SmsContactListStatus @default(ACTIVE)
|
|
createdByUserId String?
|
|
createdByUser User? @relation("SmsContactListCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
entries SmsContactListEntry[]
|
|
campaigns SmsCampaign[]
|
|
|
|
@@map("sms_contact_lists")
|
|
}
|
|
|
|
model SmsContactListEntry {
|
|
id String @id @default(cuid())
|
|
listId String
|
|
list SmsContactList @relation(fields: [listId], references: [id], onDelete: Cascade)
|
|
phone String
|
|
name String?
|
|
email String?
|
|
customFields Json? // Arbitrary key-value pairs from CSV columns
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([listId, phone])
|
|
@@index([listId])
|
|
@@index([phone])
|
|
@@map("sms_contact_list_entries")
|
|
}
|
|
|
|
model SmsCampaign {
|
|
id String @id @default(cuid())
|
|
name String
|
|
messageTemplate String @db.Text
|
|
status SmsCampaignStatus @default(DRAFT)
|
|
totalRecipients Int @default(0)
|
|
totalSent Int @default(0)
|
|
totalFailed Int @default(0)
|
|
totalResponded Int @default(0)
|
|
delayBetweenMs Int @default(3000)
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
|
|
// Relations
|
|
contactListId String
|
|
contactList SmsContactList @relation(fields: [contactListId], references: [id])
|
|
advocacyCampaignId String?
|
|
advocacyCampaign Campaign? @relation("SmsCampaigns", fields: [advocacyCampaignId], references: [id], onDelete: SetNull)
|
|
createdByUserId String?
|
|
createdByUser User? @relation("SmsCampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
recipients SmsCampaignRecipient[]
|
|
messages SmsMessage[]
|
|
conversations SmsConversation[]
|
|
|
|
@@index([status])
|
|
@@index([contactListId])
|
|
@@index([advocacyCampaignId])
|
|
@@map("sms_campaigns")
|
|
}
|
|
|
|
model SmsCampaignRecipient {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign SmsCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
|
phone String
|
|
name String?
|
|
status SmsMessageStatus @default(PENDING)
|
|
sentAt DateTime?
|
|
errorMessage String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([campaignId, status])
|
|
@@index([phone])
|
|
@@map("sms_campaign_recipients")
|
|
}
|
|
|
|
model SmsMessage {
|
|
id String @id @default(cuid())
|
|
phone String
|
|
message String @db.Text
|
|
direction SmsMessageDirection
|
|
status SmsMessageStatus @default(PENDING)
|
|
connectionType String? // e.g. "termux"
|
|
|
|
// Campaign context (nullable for ad-hoc messages)
|
|
campaignId String?
|
|
campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
|
|
conversationId String?
|
|
conversation SmsConversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull)
|
|
|
|
// Response classification (for inbound messages)
|
|
responseType SmsResponseType?
|
|
isRead Boolean @default(false)
|
|
sentAt DateTime @default(now())
|
|
|
|
@@index([phone])
|
|
@@index([campaignId])
|
|
@@index([conversationId])
|
|
@@index([direction, sentAt])
|
|
@@map("sms_messages")
|
|
}
|
|
|
|
model SmsConversation {
|
|
id String @id @default(cuid())
|
|
phone String
|
|
contactName String?
|
|
campaignId String?
|
|
campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
|
|
contactId String?
|
|
contact Contact? @relation("ContactSmsConversations", fields: [contactId], references: [id], onDelete: SetNull)
|
|
status SmsConversationStatus @default(ACTIVE)
|
|
totalMessages Int @default(0)
|
|
totalResponses Int @default(0)
|
|
unreadCount Int @default(0)
|
|
lastMessageAt DateTime?
|
|
lastResponseAt DateTime?
|
|
notes String? @db.Text
|
|
tags Json? // String array
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
messages SmsMessage[]
|
|
|
|
@@unique([phone, campaignId])
|
|
@@index([status])
|
|
@@index([lastMessageAt])
|
|
@@index([contactId])
|
|
@@map("sms_conversations")
|
|
}
|
|
|
|
model SmsMessageTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
template String @db.Text
|
|
description String?
|
|
category String?
|
|
isFavorite Boolean @default(false)
|
|
usageCount Int @default(0)
|
|
createdByUserId String?
|
|
createdByUser User? @relation("SmsTemplateCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("sms_message_templates")
|
|
}
|
|
|
|
model SmsDeviceStatus {
|
|
id String @id @default(cuid())
|
|
isConnected Boolean @default(false)
|
|
connectionType String?
|
|
batteryLevel Int?
|
|
batteryStatus String?
|
|
totalSent Int @default(0)
|
|
lastCheckedAt DateTime @default(now())
|
|
|
|
@@map("sms_device_status")
|
|
}
|
|
|
|
// ============================================================================
|
|
// PEOPLE CRM
|
|
// ============================================================================
|
|
|
|
enum ContactSource {
|
|
USER
|
|
ADDRESS_OCCUPANT
|
|
CAMPAIGN_SENDER
|
|
SHIFT_SIGNUP
|
|
SMS_CONTACT
|
|
DONATION
|
|
MANUAL
|
|
}
|
|
|
|
enum ConnectionType {
|
|
HOUSEHOLD
|
|
FAMILY
|
|
COLLEAGUE
|
|
REFERRED_BY
|
|
CUSTOM
|
|
}
|
|
|
|
enum ContactActivityType {
|
|
EMAIL_SENT
|
|
RESPONSE_SUBMITTED
|
|
SHIFT_SIGNUP
|
|
CANVASS_VISIT
|
|
DONATION
|
|
PURCHASE
|
|
SMS_SENT
|
|
SMS_RECEIVED
|
|
VIDEO_VIEW
|
|
NOTE_ADDED
|
|
CONTACT_MERGED
|
|
PROFILE_SELF_EDIT
|
|
PROFILE_PHOTO_UPDATED
|
|
USER_LOGIN
|
|
}
|
|
|
|
model Contact {
|
|
id String @id @default(cuid())
|
|
displayName String
|
|
firstName String?
|
|
lastName String?
|
|
email String?
|
|
phone String?
|
|
|
|
// CRM data
|
|
tags Json @default("[]") // String array
|
|
notes String? @db.Text
|
|
supportLevel SupportLevel?
|
|
signRequested Boolean @default(false)
|
|
|
|
// Consent
|
|
emailOptOut Boolean @default(false)
|
|
smsOptOut Boolean @default(false)
|
|
doNotContact Boolean @default(false)
|
|
|
|
// Self-service profile
|
|
profileToken String? @unique // Random hex token for public access
|
|
profileTokenExpiresAt DateTime? // null = never expires
|
|
profilePasswordHash String? // bcrypt hash; null = no password
|
|
coverPhotoPath String? // Path to processed cover photo on disk
|
|
lastSelfEditAt DateTime? // Track last self-service edit
|
|
|
|
// Source tracking
|
|
primarySource ContactSource @default(MANUAL)
|
|
|
|
// Links to existing models
|
|
userId String? @unique
|
|
user User? @relation("UserContact", fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
// Merge support
|
|
mergedIntoId String?
|
|
mergedInto Contact? @relation("ContactMerge", fields: [mergedIntoId], references: [id], onDelete: SetNull)
|
|
mergedContacts Contact[] @relation("ContactMerge")
|
|
|
|
// Audit
|
|
createdByUserId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
addresses ContactAddress[]
|
|
emails ContactEmail[]
|
|
phones ContactPhone[]
|
|
connectionsFrom ContactConnection[] @relation("ConnectionFrom")
|
|
connectionsTo ContactConnection[] @relation("ConnectionTo")
|
|
activities ContactActivity[]
|
|
smsConversations SmsConversation[] @relation("ContactSmsConversations")
|
|
|
|
@@index([email])
|
|
@@index([phone])
|
|
@@index([displayName])
|
|
@@index([primarySource])
|
|
@@index([mergedIntoId])
|
|
@@map("contacts")
|
|
}
|
|
|
|
model CrmTag {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
slug String @unique
|
|
description String?
|
|
color String? // Hex, e.g. "#1890ff"
|
|
listmonkListId Int? // Corresponding Listmonk list ID
|
|
contactCount Int @default(0) // Denormalized count
|
|
createdByUserId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([name])
|
|
@@map("crm_tags")
|
|
}
|
|
|
|
model ContactAddress {
|
|
id String @id @default(cuid())
|
|
contactId String
|
|
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
|
addressId String
|
|
address Address @relation(fields: [addressId], references: [id], onDelete: Cascade)
|
|
isPrimary Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([contactId, addressId])
|
|
@@index([addressId])
|
|
@@map("contact_addresses")
|
|
}
|
|
|
|
model ContactEmail {
|
|
id String @id @default(cuid())
|
|
contactId String
|
|
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
|
email String
|
|
label String? // "Personal", "Work", "Campaign", etc.
|
|
isPrimary Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([contactId, email])
|
|
@@index([email])
|
|
@@index([contactId])
|
|
@@map("contact_emails")
|
|
}
|
|
|
|
model ContactPhone {
|
|
id String @id @default(cuid())
|
|
contactId String
|
|
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
|
phone String
|
|
label String? // "Mobile", "Home", "Work", etc.
|
|
isPrimary Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([contactId, phone])
|
|
@@index([phone])
|
|
@@index([contactId])
|
|
@@map("contact_phones")
|
|
}
|
|
|
|
model ContactConnection {
|
|
id String @id @default(cuid())
|
|
fromContactId String
|
|
fromContact Contact @relation("ConnectionFrom", fields: [fromContactId], references: [id], onDelete: Cascade)
|
|
toContactId String
|
|
toContact Contact @relation("ConnectionTo", fields: [toContactId], references: [id], onDelete: Cascade)
|
|
type ConnectionType
|
|
label String?
|
|
notes String? @db.Text
|
|
isBidirectional Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([fromContactId, toContactId, type])
|
|
@@index([toContactId])
|
|
@@map("contact_connections")
|
|
}
|
|
|
|
model ContactActivity {
|
|
id String @id @default(cuid())
|
|
contactId String
|
|
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
|
type ContactActivityType
|
|
title String
|
|
description String? @db.Text
|
|
metadata Json?
|
|
occurredAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([contactId, occurredAt(sort: Desc)])
|
|
@@map("contact_activities")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MEETINGS (JITSI)
|
|
// ============================================================================
|
|
|
|
model Meeting {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
title String
|
|
description String?
|
|
jitsiRoom String @unique @map("jitsi_room")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdByUserId String @map("created_by_user_id")
|
|
createdBy User @relation("MeetingCreator", fields: [createdByUserId], references: [id])
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
startTime DateTime? @map("start_time")
|
|
endTime DateTime? @map("end_time")
|
|
|
|
// Reverse relations (one-to-one)
|
|
shift Shift? @relation("ShiftMeeting")
|
|
group SocialGroup? @relation("GroupMeeting")
|
|
|
|
@@map("meetings")
|
|
}
|