generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================================================ // AUTH & USERS // ============================================================================ enum UserRole { SUPER_ADMIN INFLUENCE_ADMIN MAP_ADMIN BROADCAST_ADMIN CONTENT_ADMIN MEDIA_ADMIN PAYMENTS_ADMIN EVENTS_ADMIN SOCIAL_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? pronouns 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") // Scheduling polls schedulingPollsCreated SchedulingPoll[] @relation("PollCreator") schedulingPollVotes SchedulingPollVote[] @relation("PollVoter") schedulingPollComments SchedulingPollComment[] @relation("PollCommenter") // Participant needs participantNeeds ParticipantNeeds? @relation("UserParticipantNeeds") // Meeting agendas & action items agendasCreated MeetingAgenda[] @relation("AgendaCreator") minutesCreated MeetingMinutes[] @relation("MinutesCreator") minutesApproved MeetingMinutes[] @relation("MinutesApprover") actionItemsAssigned ActionItem[] @relation("ActionItemAssignee") actionItemsCreated ActionItem[] @relation("ActionItemCreator") // Referral system inviteCodesCreated InviteCode[] @relation("InviteCodesCreated") referralsMade Referral[] @relation("ReferralsMade") referredBy Referral? @relation("ReferredBy") // Impact Stories impactStoriesCreated ImpactStory[] @relation("ImpactStoryCreator") // Volunteer Spotlight spotlights VolunteerSpotlight[] @relation("SpotlightUser") spotlightNominations VolunteerSpotlight[] @relation("SpotlightNominator") spotlightApprovals VolunteerSpotlight[] @relation("SpotlightApprover") // Team Challenges challengesCreated Challenge[] @relation("ChallengesCreated") challengeTeamsCaptained ChallengeTeam[] @relation("ChallengeTeamsCaptained") challengeParticipations ChallengeTeamMember[] @relation("ChallengeParticipations") // Ticketed Events ticketedEventsCreated TicketedEvent[] @relation("EventCreator") ticketsHeld Ticket[] @relation("TicketHolder") checkInsMade CheckIn[] @relation("CheckInUser") // Social Calendar calendarLayers CalendarLayer[] @relation("CalendarLayerOwner") calendarItems CalendarItem[] @relation("CalendarItemOwner") calendarFeeds CalendarFeed[] @relation("CalendarFeedOwner") sharedCalendarViewsOwned SharedCalendarView[] @relation("SharedViewOwner") sharedCalendarMemberships SharedCalendarMember[] @relation("SharedViewMember") sharedViewComments SharedViewComment[] @relation("SharedViewCommentUser") sharedViewReactions SharedViewReaction[] @relation("SharedViewReactionUser") calendarExportTokens CalendarExportToken[] @relation("CalendarExportTokenOwner") // Docs access & sharing docAccessPoliciesCreated DocAccessPolicy[] @relation("DocAccessPolicyCreator") docShareLinksCreated DocShareLink[] @relation("DocShareLinkCreator") docWatches DocWatch[] @relation("DocWatcher") @@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") stories ImpactStory[] @relation("CampaignStories") milestones CampaignMilestone[] @relation("CampaignMilestones") donationOrders Order[] @relation("CampaignDonations") @@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[] // Scheduling poll conversion convertedFromPoll SchedulingPoll? @relation("PollConvertedShift") // Meeting agenda agenda MeetingAgenda? @relation("ShiftAgenda") @@index([cutId]) @@index([seriesId]) @@map("shifts") } enum SignupStatus { CONFIRMED CANCELLED } enum SignupSource { AUTHENTICATED PUBLIC ADMIN POLL_CONVERSION } 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") enableMeetingPlanner Boolean @default(false) @map("enable_meeting_planner") enableTicketedEvents Boolean @default(false) @map("enable_ticketed_events") enableSocialCalendar Boolean @default(false) @map("enable_social_calendar") enableDocsCollaboration Boolean @default(false) @map("enable_docs_collaboration") requireEventApproval Boolean @default(true) @map("require_event_approval") 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 giteaSetupComplete Boolean @default(false) @map("gitea_setup_complete") // 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") // Auto-upgrade settings enableAutoUpgrade Boolean @default(false) @map("enable_auto_upgrade") autoUpgradeSchedule String @default("daily-3am") @map("auto_upgrade_schedule") autoUpgradePullServices Boolean @default(false) @map("auto_upgrade_pull_services") notifyAdminAutoUpgrade Boolean @default(true) @map("notify_admin_auto_upgrade") useRegistryForUpgrade Boolean @default(false) @map("use_registry_for_upgrade") giteaRegistryUrl String @default("gitea.bnkops.com/admin") @map("gitea_registry_url") // 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 impact_story referral_completed challenge_update shared_view_invite shared_view_accepted calendar_event_invite // Operational notification types shift_signup_confirmed shift_reminder shift_cancelled canvass_session_summary reengagement } // ============================================================================ // 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") showOnLeaderboard Boolean? @default(true) @map("show_on_leaderboard") 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) influenceCampaignId String? @map("influence_campaign_id") influenceCampaign Campaign? @relation("CampaignDonations", fields: [influenceCampaignId], references: [id], onDelete: SetNull) tickets Ticket[] @relation("TicketOrder") @@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") @@index([influenceCampaignId], map: "idx_orders_influence_campaign") @@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 POLL_VOTE 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 POLL_VOTED } model Contact { id String @id @default(cuid()) displayName String firstName String? lastName String? email String? phone String? pronouns 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") pollVotes SchedulingPollVote[] @relation("PollVoteContact") participantNeeds ParticipantNeeds? @relation("ContactParticipantNeeds") @@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") ticketedEvent TicketedEvent? @relation("EventMeeting") @@map("meetings") } // ============================================================================ // SCHEDULING POLLS (Meeting Planner) // ============================================================================ enum SchedulingPollStatus { OPEN CLOSED FINALIZED CANCELLED } enum PollVoteValue { YES IF_NEED_BE NO } model SchedulingPoll { id String @id @default(cuid()) slug String @unique title String description String? @db.Text location String? status SchedulingPollStatus @default(OPEN) timezone String @default("America/Edmonton") finalizedOptionId String? @unique @map("finalized_option_id") finalizedOption SchedulingPollOption? @relation("FinalizedOption", fields: [finalizedOptionId], references: [id], onDelete: SetNull) convertedShiftId String? @unique @map("converted_shift_id") convertedShift Shift? @relation("PollConvertedShift", fields: [convertedShiftId], references: [id], onDelete: SetNull) convertedGancioEventId Int? @map("converted_gancio_event_id") convertedCalendarItemId String? @map("converted_calendar_item_id") votingDeadline DateTime? @map("voting_deadline") autoFinalize Boolean @default(false) @map("auto_finalize") autoFinalizeThreshold Int? @map("auto_finalize_threshold") autoConvertToCalendar Boolean @default(false) @map("auto_convert_to_calendar") autoConvertToGancio Boolean @default(false) @map("auto_convert_to_gancio") autoConvertToShift Boolean @default(false) @map("auto_convert_to_shift") tieBreaker String @default("earliest") @map("tie_breaker") autoEnrollVoters Boolean @default(true) @map("auto_enroll_voters") autoFinalizeJobId String? @map("auto_finalize_job_id") allowAnonymous Boolean @default(true) @map("allow_anonymous") isPrivate Boolean @default(false) @map("is_private") notifyOnVote Boolean @default(true) @map("notify_on_vote") createdByUserId String @map("created_by_user_id") createdBy User @relation("PollCreator", fields: [createdByUserId], references: [id]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") options SchedulingPollOption[] @relation("PollOptions") votes SchedulingPollVote[] @relation("PollVotes") comments SchedulingPollComment[] @relation("PollComments") agenda MeetingAgenda? @relation("PollAgenda") @@index([createdByUserId]) @@index([status]) @@map("scheduling_polls") } model SchedulingPollOption { id String @id @default(cuid()) pollId String @map("poll_id") poll SchedulingPoll @relation("PollOptions", fields: [pollId], references: [id], onDelete: Cascade) date DateTime @db.Date startTime String @map("start_time") // HH:MM endTime String @map("end_time") // HH:MM sortOrder Int @default(0) @map("sort_order") votes SchedulingPollVote[] @relation("OptionVotes") // Reverse 1:1 for finalized option finalizedForPoll SchedulingPoll? @relation("FinalizedOption") @@index([pollId]) @@map("scheduling_poll_options") } model SchedulingPollVote { id String @id @default(cuid()) pollId String @map("poll_id") poll SchedulingPoll @relation("PollVotes", fields: [pollId], references: [id], onDelete: Cascade) optionId String @map("option_id") option SchedulingPollOption @relation("OptionVotes", fields: [optionId], references: [id], onDelete: Cascade) userId String? @map("user_id") user User? @relation("PollVoter", fields: [userId], references: [id], onDelete: SetNull) voterName String @map("voter_name") voterEmail String? @map("voter_email") voterToken String? @map("voter_token") // anonymous edit access (cuid) contactId String? @map("contact_id") contact Contact? @relation("PollVoteContact", fields: [contactId], references: [id], onDelete: SetNull) value PollVoteValue createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([optionId, userId]) @@unique([optionId, voterToken]) @@index([pollId]) @@index([contactId]) @@map("scheduling_poll_votes") } model SchedulingPollComment { id String @id @default(cuid()) pollId String @map("poll_id") poll SchedulingPoll @relation("PollComments", fields: [pollId], references: [id], onDelete: Cascade) userId String? @map("user_id") user User? @relation("PollCommenter", fields: [userId], references: [id], onDelete: SetNull) authorName String @map("author_name") content String @db.Text createdAt DateTime @default(now()) @map("created_at") @@index([pollId]) @@map("scheduling_poll_comments") } // ============================================================================ // SOCIAL: INVITE / REFERRAL SYSTEM // ============================================================================ model InviteCode { id String @id @default(cuid()) code String @unique createdByUserId String @map("created_by_user_id") maxUses Int @default(0) @map("max_uses") // 0 = unlimited usedCount Int @default(0) @map("used_count") expiresAt DateTime? @map("expires_at") isActive Boolean @default(true) @map("is_active") note String? createdAt DateTime @default(now()) @map("created_at") // Relations createdBy User @relation("InviteCodesCreated", fields: [createdByUserId], references: [id]) referrals Referral[] @relation("InviteCodeReferrals") @@index([code], map: "idx_invite_codes_code") @@index([createdByUserId], map: "idx_invite_codes_created_by") @@map("invite_codes") } model Referral { id Int @id @default(autoincrement()) referrerId String @map("referrer_id") referredUserId String @unique @map("referred_user_id") inviteCodeId String? @map("invite_code_id") referralSource String? @map("referral_source") completedAt DateTime @default(now()) @map("completed_at") // Relations referrer User @relation("ReferralsMade", fields: [referrerId], references: [id]) referredUser User @relation("ReferredBy", fields: [referredUserId], references: [id]) inviteCode InviteCode? @relation("InviteCodeReferrals", fields: [inviteCodeId], references: [id]) @@index([referrerId], map: "idx_referrals_referrer") @@map("referrals") } // ============================================================================ // SOCIAL: IMPACT STORIES / CAMPAIGN VICTORIES // ============================================================================ enum ImpactStoryType { MILESTONE VICTORY RESPONSE CUSTOM } enum ImpactStoryStatus { DRAFT PUBLISHED ARCHIVED } model ImpactStory { id String @id @default(cuid()) campaignId String @map("campaign_id") type ImpactStoryType status ImpactStoryStatus @default(DRAFT) title String body String @db.Text coverImageUrl String? @map("cover_image_url") milestoneValue Int? @map("milestone_value") milestoneMetric String? @map("milestone_metric") createdByUserId String? @map("created_by_user_id") publishedAt DateTime? @map("published_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations campaign Campaign @relation("CampaignStories", fields: [campaignId], references: [id]) createdBy User? @relation("ImpactStoryCreator", fields: [createdByUserId], references: [id]) @@index([campaignId], map: "idx_impact_stories_campaign") @@index([status], map: "idx_impact_stories_status") @@index([type], map: "idx_impact_stories_type") @@map("impact_stories") } model CampaignMilestone { id Int @id @default(autoincrement()) campaignId String @map("campaign_id") metric String // "emails_sent", "verified_responses" threshold Int reachedAt DateTime @default(now()) @map("reached_at") storyGenerated Boolean @default(false) @map("story_generated") // Relations campaign Campaign @relation("CampaignMilestones", fields: [campaignId], references: [id]) @@unique([campaignId, metric, threshold]) @@map("campaign_milestones") } // ============================================================================ // SOCIAL: VOLUNTEER SPOTLIGHT / WALL OF FAME // ============================================================================ enum SpotlightStatus { NOMINATED APPROVED FEATURED ARCHIVED } model VolunteerSpotlight { id String @id @default(cuid()) userId String @map("user_id") status SpotlightStatus @default(NOMINATED) headline String? story String? @db.Text featuredMonth String? @map("featured_month") // "2026-03" nominatedByUserId String? @map("nominated_by_user_id") approvedByUserId String? @map("approved_by_user_id") approvedAt DateTime? @map("approved_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation("SpotlightUser", fields: [userId], references: [id]) nominatedBy User? @relation("SpotlightNominator", fields: [nominatedByUserId], references: [id]) approvedBy User? @relation("SpotlightApprover", fields: [approvedByUserId], references: [id]) @@index([userId], map: "idx_volunteer_spotlights_user") @@index([status], map: "idx_volunteer_spotlights_status") @@index([featuredMonth], map: "idx_volunteer_spotlights_month") @@map("volunteer_spotlights") } // ============================================================================ // SOCIAL: TEAM CHALLENGES // ============================================================================ enum ChallengeStatus { DRAFT UPCOMING ACTIVE COMPLETED CANCELLED } enum ChallengeMetric { DOORS_KNOCKED EMAILS_SENT SHIFTS_ATTENDED RESPONSES_SUBMITTED REFERRALS_MADE } model Challenge { id String @id @default(cuid()) title String description String? @db.Text metric ChallengeMetric status ChallengeStatus @default(DRAFT) startsAt DateTime @map("starts_at") endsAt DateTime @map("ends_at") minTeamSize Int @default(2) @map("min_team_size") maxTeamSize Int @default(10) @map("max_team_size") maxTeams Int? @map("max_teams") // null = unlimited createdByUserId String @map("created_by_user_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations createdBy User @relation("ChallengesCreated", fields: [createdByUserId], references: [id]) teams ChallengeTeam[] @relation("ChallengeTeams") @@index([status], map: "idx_challenges_status") @@index([startsAt], map: "idx_challenges_starts_at") @@map("challenges") } model ChallengeTeam { id String @id @default(cuid()) challengeId String @map("challenge_id") name String captainUserId String @map("captain_user_id") score Int @default(0) lastScoredAt DateTime? @map("last_scored_at") createdAt DateTime @default(now()) @map("created_at") // Relations challenge Challenge @relation("ChallengeTeams", fields: [challengeId], references: [id]) captain User @relation("ChallengeTeamsCaptained", fields: [captainUserId], references: [id]) members ChallengeTeamMember[] @relation("ChallengeTeamMembers") @@unique([challengeId, name]) @@index([challengeId], map: "idx_challenge_teams_challenge") @@index([score], map: "idx_challenge_teams_score") @@map("challenge_teams") } model ChallengeTeamMember { id Int @id @default(autoincrement()) teamId String @map("team_id") userId String @map("user_id") score Int @default(0) joinedAt DateTime @default(now()) @map("joined_at") // Relations team ChallengeTeam @relation("ChallengeTeamMembers", fields: [teamId], references: [id], onDelete: Cascade) user User @relation("ChallengeParticipations", fields: [userId], references: [id]) @@unique([teamId, userId]) @@index([userId], map: "idx_challenge_team_members_user") @@map("challenge_team_members") } // ============================================================================ // TICKETED EVENTS // ============================================================================ enum TicketedEventStatus { DRAFT PENDING_APPROVAL PUBLISHED CANCELLED COMPLETED } enum TicketedEventVisibility { PUBLIC UNLISTED PRIVATE } enum TicketTierType { PAID FREE DONATION } enum TicketStatus { VALID CHECKED_IN CANCELLED REFUNDED } enum EventFormat { IN_PERSON ONLINE HYBRID } model TicketedEvent { id String @id @default(cuid()) slug String @unique title String description String? @db.Text richDescription String? @db.Text @map("rich_description") // Schedule date DateTime @db.Date startTime String @map("start_time") endTime String @map("end_time") doorsOpenTime String? @map("doors_open_time") // Venue venueName String? @map("venue_name") venueAddress String? @map("venue_address") latitude Decimal? @db.Decimal(10, 7) longitude Decimal? @db.Decimal(10, 7) // Status status TicketedEventStatus @default(DRAFT) visibility TicketedEventVisibility @default(PUBLIC) inviteCode String? @unique @map("invite_code") // Media coverImageUrl String? @map("cover_image_url") // Capacity maxAttendees Int? @map("max_attendees") currentAttendees Int @default(0) @map("current_attendees") // Gancio sync gancioEventId Int? @map("gancio_event_id") // Format & Meeting eventFormat EventFormat @default(IN_PERSON) @map("event_format") meetingId String? @unique @map("meeting_id") // Creator createdByUserId String @map("created_by_user_id") organizerName String? @map("organizer_name") organizerEmail String? @map("organizer_email") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations createdBy User @relation("EventCreator", fields: [createdByUserId], references: [id]) meeting Meeting? @relation("EventMeeting", fields: [meetingId], references: [id], onDelete: SetNull) ticketTiers TicketTier[] @relation("EventTiers") tickets Ticket[] @relation("EventTickets") checkIns CheckIn[] @relation("EventCheckIns") @@index([status], map: "idx_ticketed_events_status") @@index([date], map: "idx_ticketed_events_date") @@index([visibility], map: "idx_ticketed_events_visibility") @@index([createdByUserId], map: "idx_ticketed_events_creator") @@map("ticketed_events") } model TicketTier { id String @id @default(cuid()) eventId String @map("event_id") name String description String? tierType TicketTierType @map("tier_type") priceCAD Int @default(0) @map("price_cad") // In cents minDonationCAD Int? @map("min_donation_cad") // In cents maxQuantity Int? @map("max_quantity") soldCount Int @default(0) @map("sold_count") reservedCount Int @default(0) @map("reserved_count") // Pending checkout sessions maxPerOrder Int @default(10) @map("max_per_order") salesStartAt DateTime? @map("sales_start_at") salesEndAt DateTime? @map("sales_end_at") sortOrder Int @default(0) @map("sort_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations event TicketedEvent @relation("EventTiers", fields: [eventId], references: [id], onDelete: Cascade) tickets Ticket[] @relation("TierTickets") @@index([eventId], map: "idx_ticket_tiers_event") @@map("ticket_tiers") } model Ticket { id String @id @default(cuid()) ticketCode String @unique @map("ticket_code") tokenHash String @unique @map("token_hash") eventId String @map("event_id") tierId String @map("tier_id") orderId String? @map("order_id") holderEmail String @map("holder_email") holderName String? @map("holder_name") userId String? @map("user_id") status TicketStatus @default(VALID) checkedInAt DateTime? @map("checked_in_at") checkedInByUserId String? @map("checked_in_by_user_id") issuedAt DateTime @default(now()) @map("issued_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations event TicketedEvent @relation("EventTickets", fields: [eventId], references: [id]) tier TicketTier @relation("TierTickets", fields: [tierId], references: [id]) order Order? @relation("TicketOrder", fields: [orderId], references: [id]) holder User? @relation("TicketHolder", fields: [userId], references: [id]) checkIns CheckIn[] @relation("TicketCheckIns") @@index([eventId], map: "idx_tickets_event") @@index([tierId], map: "idx_tickets_tier") @@index([orderId], map: "idx_tickets_order") @@index([holderEmail], map: "idx_tickets_holder_email") @@index([status], map: "idx_tickets_status") @@map("tickets") } model CheckIn { id String @id @default(cuid()) ticketId String @map("ticket_id") eventId String @map("event_id") checkedInByUserId String? @map("checked_in_by_user_id") method String // "QR" | "MANUAL" | "CODE" checkedInAt DateTime @default(now()) @map("checked_in_at") notes String? // Relations ticket Ticket @relation("TicketCheckIns", fields: [ticketId], references: [id]) event TicketedEvent @relation("EventCheckIns", fields: [eventId], references: [id]) checkedInBy User? @relation("CheckInUser", fields: [checkedInByUserId], references: [id]) @@index([eventId], map: "idx_checkins_event") @@index([ticketId], map: "idx_checkins_ticket") @@map("check_ins") } // ============================================================================ // SOCIAL CALENDAR // ============================================================================ enum CalendarLayerType { SYSTEM USER EXTERNAL } enum CalendarSystemType { SHIFTS TICKETS POLLS PUBLIC_EVENTS } enum CalendarVisibility { PRIVATE FRIENDS PUBLIC } enum CalendarItemType { EVENT TIME_BLOCK REMINDER } enum CalendarBusyStatus { BUSY TENTATIVE FREE } enum CalendarShowDetailsTo { NOBODY FRIENDS EVERYONE } enum CalendarItemSource { MANUAL ICS_FEED POLL } enum CalendarRecurrenceFrequency { DAILY WEEKLY BIWEEKLY MONTHLY } enum CalendarFeedStatus { OK ERROR PENDING } enum CalendarFeedInterval { FIFTEEN_MIN HOURLY SIX_HOUR DAILY } enum SharedViewType { MANUAL ROLE_BASED } enum SharedViewScope { MEMBERS PUBLIC } enum SharedViewMemberStatus { INVITED ACCEPTED DECLINED } model CalendarLayer { id String @id @default(cuid()) userId String @map("user_id") name String layerType CalendarLayerType @map("layer_type") systemType CalendarSystemType? @map("system_type") color String @default("#1890ff") visibility CalendarVisibility @default(PRIVATE) isEnabled Boolean @default(true) @map("is_enabled") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation("CalendarLayerOwner", fields: [userId], references: [id], onDelete: Cascade) items CalendarItem[] @relation("CalendarLayerItems") feed CalendarFeed? @relation("CalendarFeedLayer") @@unique([userId, systemType], map: "idx_calendar_layers_user_system") @@index([userId], map: "idx_calendar_layers_user") @@map("calendar_layers") } model CalendarItem { id String @id @default(cuid()) userId String @map("user_id") layerId String @map("layer_id") title String description String? @db.Text date DateTime @db.Date startTime String @map("start_time") // HH:MM endTime String @map("end_time") // HH:MM isAllDay Boolean @default(false) @map("is_all_day") itemType CalendarItemType @default(EVENT) @map("item_type") location String? color String? visibility CalendarVisibility? // null = inherit from layer busyStatus CalendarBusyStatus @default(BUSY) @map("busy_status") showDetailsTo CalendarShowDetailsTo @default(FRIENDS) @map("show_details_to") // Recurrence recurrenceRule Json? @map("recurrence_rule") recurrenceEnd DateTime? @map("recurrence_end") seriesId String? @map("series_id") isException Boolean @default(false) @map("is_exception") // Source tracking sourceType CalendarItemSource @default(MANUAL) @map("source_type") sourceId String? @map("source_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation("CalendarItemOwner", fields: [userId], references: [id], onDelete: Cascade) layer CalendarLayer @relation("CalendarLayerItems", fields: [layerId], references: [id], onDelete: Cascade) @@index([userId, date], map: "idx_calendar_items_user_date") @@index([layerId, date], map: "idx_calendar_items_layer_date") @@index([seriesId], map: "idx_calendar_items_series") @@index([sourceType, sourceId], map: "idx_calendar_items_source") @@map("calendar_items") } model CalendarFeed { id String @id @default(cuid()) userId String @map("user_id") name String url String layerId String @unique @map("layer_id") refreshInterval CalendarFeedInterval @default(HOURLY) @map("refresh_interval") lastFetchedAt DateTime? @map("last_fetched_at") lastStatus CalendarFeedStatus @default(PENDING) @map("last_status") lastError String? @map("last_error") itemCount Int @default(0) @map("item_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation("CalendarFeedOwner", fields: [userId], references: [id], onDelete: Cascade) layer CalendarLayer @relation("CalendarFeedLayer", fields: [layerId], references: [id], onDelete: Cascade) @@index([userId], map: "idx_calendar_feeds_user") @@map("calendar_feeds") } model SharedCalendarView { id String @id @default(cuid()) name String description String? @db.Text ownerId String @map("owner_id") viewType SharedViewType @default(MANUAL) @map("view_type") autoIncludeRoles Json? @map("auto_include_roles") includedLayerTypes Json @default("[]") @map("included_layer_types") shareScope SharedViewScope @default(MEMBERS) @map("share_scope") shareToken String? @unique @map("share_token") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations owner User @relation("SharedViewOwner", fields: [ownerId], references: [id], onDelete: Cascade) members SharedCalendarMember[] @relation("SharedViewMembers") comments SharedViewComment[] @relation("SharedViewComments") reactions SharedViewReaction[] @relation("SharedViewReactions") @@index([ownerId], map: "idx_shared_views_owner") @@map("shared_calendar_views") } model SharedCalendarMember { id String @id @default(cuid()) viewId String @map("view_id") userId String @map("user_id") status SharedViewMemberStatus @default(INVITED) color String @default("#1890ff") joinedAt DateTime? @map("joined_at") // Relations view SharedCalendarView @relation("SharedViewMembers", fields: [viewId], references: [id], onDelete: Cascade) user User @relation("SharedViewMember", fields: [userId], references: [id], onDelete: Cascade) @@unique([viewId, userId], map: "idx_shared_members_view_user") @@index([userId], map: "idx_shared_members_user") @@map("shared_calendar_members") } model SharedViewComment { id String @id @default(cuid()) viewId String @map("view_id") userId String @map("user_id") itemDate String @map("item_date") // YYYY-MM-DD itemId String? @map("item_id") // specific item reference content String @db.Text createdAt DateTime @default(now()) @map("created_at") // Relations view SharedCalendarView @relation("SharedViewComments", fields: [viewId], references: [id], onDelete: Cascade) user User @relation("SharedViewCommentUser", fields: [userId], references: [id], onDelete: Cascade) @@index([viewId, itemDate], map: "idx_shared_comments_view_date") @@map("shared_view_comments") } model SharedViewReaction { id String @id @default(cuid()) viewId String @map("view_id") userId String @map("user_id") itemId String @map("item_id") emoji String createdAt DateTime @default(now()) @map("created_at") // Relations view SharedCalendarView @relation("SharedViewReactions", fields: [viewId], references: [id], onDelete: Cascade) user User @relation("SharedViewReactionUser", fields: [userId], references: [id], onDelete: Cascade) @@unique([viewId, userId, itemId, emoji], map: "idx_shared_reactions_unique") @@map("shared_view_reactions") } model CalendarExportToken { id String @id @default(cuid()) userId String @map("user_id") token String @unique includePersonal Boolean @default(false) @map("include_personal") includeLayers Json? @map("include_layers") createdAt DateTime @default(now()) @map("created_at") // Relations user User @relation("CalendarExportTokenOwner", fields: [userId], references: [id], onDelete: Cascade) @@index([userId], map: "idx_calendar_export_tokens_user") @@map("calendar_export_tokens") } // ============================================================================ // DOCS COLLABORATION // ============================================================================ model DocCollabState { id String @id @default(cuid()) documentId String @unique @map("document_id") // file path, e.g. "admin/index.md" state Bytes // Y.Doc binary state updatedAt DateTime @updatedAt @map("updated_at") createdAt DateTime @default(now()) @map("created_at") @@map("doc_collab_state") } // --- Document Access Policies --- enum DocShareLinkStatus { ACTIVE REVOKED EXPIRED } model DocAccessPolicy { id String @id @default(cuid()) documentPath String @unique @map("document_path") // e.g. "admin/index.md" or "guides/" (trailing slash = directory) isDirectory Boolean @default(false) @map("is_directory") allowedEditors Json @default("[]") @map("allowed_editors") // ["role:CONTENT_ADMIN", "user:clxyz", "all_content_editors"] createdById String @map("created_by_id") createdBy User @relation("DocAccessPolicyCreator", fields: [createdById], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([createdById]) @@map("doc_access_policies") } model DocShareLink { id String @id @default(cuid()) documentPath String @map("document_path") shareToken String @unique @map("share_token") status DocShareLinkStatus @default(ACTIVE) canEdit Boolean @default(true) @map("can_edit") expiresAt DateTime? @map("expires_at") maxUses Int? @map("max_uses") useCount Int @default(0) @map("use_count") guestName String? @map("guest_name") createdById String @map("created_by_id") createdBy User @relation("DocShareLinkCreator", fields: [createdById], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([documentPath]) @@index([createdById]) @@map("doc_share_links") } model DocWatch { id String @id @default(cuid()) userId String @map("user_id") filePath String @map("file_path") user User @relation("DocWatcher", fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@unique([userId, filePath]) @@index([filePath]) @@map("doc_watches") } // ============================================================================ // PARTICIPANT NEEDS // ============================================================================ model ParticipantNeeds { id String @id @default(cuid()) userId String? @unique @map("user_id") user User? @relation("UserParticipantNeeds", fields: [userId], references: [id], onDelete: SetNull) contactId String? @unique @map("contact_id") contact Contact? @relation("ContactParticipantNeeds", fields: [contactId], references: [id], onDelete: SetNull) // Accessibility needsWheelchair Boolean @default(false) @map("needs_wheelchair") needsGroundFloor Boolean @default(false) @map("needs_ground_floor") needsHearingLoop Boolean @default(false) @map("needs_hearing_loop") needsSignLanguage Boolean @default(false) @map("needs_sign_language") otherAccessibility String? @db.Text @map("other_accessibility") // Dietary isVegan Boolean @default(false) @map("is_vegan") isVegetarian Boolean @default(false) @map("is_vegetarian") isGlutenFree Boolean @default(false) @map("is_gluten_free") isHalal Boolean @default(false) @map("is_halal") isKosher Boolean @default(false) @map("is_kosher") hasNutAllergy Boolean @default(false) @map("has_nut_allergy") otherDietary String? @db.Text @map("other_dietary") // Care barriers needsChildcare Boolean @default(false) @map("needs_childcare") childcareDetails String? @db.Text @map("childcare_details") needsTransportation Boolean @default(false) @map("needs_transportation") transportationNotes String? @db.Text @map("transportation_notes") // Communication preferredLanguage String? @default("en") @map("preferred_language") needsTranslation Boolean @default(false) @map("needs_translation") translationLanguage String? @map("translation_language") // Consent visibilityConsent String @default("organizer_only") @map("visibility_consent") updatedAt DateTime @updatedAt @map("updated_at") @@map("participant_needs") } // ============================================================================ // MEETING AGENDAS & ACTION ITEMS // ============================================================================ model MeetingAgenda { id String @id @default(cuid()) shiftId String? @unique @map("shift_id") shift Shift? @relation("ShiftAgenda", fields: [shiftId], references: [id], onDelete: SetNull) pollId String? @unique @map("poll_id") poll SchedulingPoll? @relation("PollAgenda", fields: [pollId], references: [id], onDelete: SetNull) title String items Json @default("[]") // Array<{ id, title, durationMinutes, presenterUserId, notes, order }> status String @default("draft") // "draft" | "active" | "completed" minutes MeetingMinutes? actionItems ActionItem[] createdByUserId String @map("created_by_user_id") createdBy User @relation("AgendaCreator", fields: [createdByUserId], references: [id]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("meeting_agendas") } model MeetingMinutes { id String @id @default(cuid()) agendaId String @unique @map("agenda_id") agenda MeetingAgenda @relation(fields: [agendaId], references: [id], onDelete: Cascade) notes String @db.Text decisions Json @default("[]") // Array<{ id, text, passed: boolean }> attendees Json @default("[]") // Array<{ name, pronouns, userId? }> approvedAt DateTime? @map("approved_at") approvedByUserId String? @map("approved_by_user_id") approvedBy User? @relation("MinutesApprover", fields: [approvedByUserId], references: [id], onDelete: SetNull) createdByUserId String @map("created_by_user_id") createdBy User @relation("MinutesCreator", fields: [createdByUserId], references: [id]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("meeting_minutes") } model ActionItem { id String @id @default(cuid()) agendaId String? @map("agenda_id") agenda MeetingAgenda? @relation(fields: [agendaId], references: [id], onDelete: SetNull) title String description String? @db.Text assigneeUserId String? @map("assignee_user_id") assignee User? @relation("ActionItemAssignee", fields: [assigneeUserId], references: [id], onDelete: SetNull) dueDate DateTime? @map("due_date") status String @default("open") // "open" | "in_progress" | "done" | "blocked" priority String @default("normal") // "low" | "normal" | "high" | "urgent" completedAt DateTime? @map("completed_at") createdByUserId String @map("created_by_user_id") createdBy User @relation("ActionItemCreator", fields: [createdByUserId], references: [id]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([assigneeUserId, status]) @@index([dueDate]) @@map("action_items") }