datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model User { id String @id @default(cuid()) email String @unique passwordHash String name String? role String @default("SEEKER") // SEEKER, EMPLOYER, ADMIN createdAt DateTime @default(now()) updatedAt DateTime @updatedAt resumes Resume[] interactions UserJobInteraction[] profile UserProfile? applications Application[] jobAlerts JobAlert[] savedJobs SavedJob[] savedSearches SavedSearch[] statusChanges ApplicationStatusHistory[] authoredNotes CandidateNote[] createdTags CandidateTag[] notifications Notification[] notificationPreference NotificationPreference? companyReviews CompanyReview[] companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) postedJobs Job[] @relation("PostedJobs") auditLogs AuditLog[] @relation("ActorAuditLogs") emailVerified DateTime? failedLoginAttempts Int @default(0) lockedUntil DateTime? passwordResetTokens PasswordResetToken[] memberships OrganizationMembership[] createdInvitations OrganizationInvitation[] @relation("InvitedBy") companyClaims CompanyClaim[] @relation("Claimant") analyses JobAnalysisCache[] } model UserProfile { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) headline String? bio String? phone String? location String? isPublic Boolean @default(true) searchableToEmployers Boolean @default(false) skills CandidateSkill[] workHistory WorkExperience[] education Education[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model CandidateSkill { id String @id @default(cuid()) profileId String profile UserProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) name String level String? // BEGINNER, INTERMEDIATE, EXPERT } model WorkExperience { id String @id @default(cuid()) profileId String profile UserProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) company String title String location String? startDate DateTime endDate DateTime? isCurrent Boolean @default(false) description String? } model Education { id String @id @default(cuid()) profileId String profile UserProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) institution String degree String fieldOfStudy String? startDate DateTime? endDate DateTime? } model Resume { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) title String @default("My Resume") data String @default("{}") isActiveForMatching Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) } model Job { id String @id @default(cuid()) jobUrlHash String @unique fingerprintHash String? // sha256(canonicalCompany + canonicalTitle + location + type) title String company String location String isRemote Boolean @default(false) department String? experienceLevel String? description String salaryMin Float? salaryMax Float? jobUrl String source String sourceId String? // External source ID in JobSource registry moderationStatus String @default("APPROVED") // APPROVED, PENDING_REVIEW, REJECTED, FLAGGED moderationReason String? moderatedAt DateTime? lifecycleStatus String @default("ACTIVE") // ACTIVE, STALE, EXPIRED, ARCHIVED firstSeenAt DateTime @default(now()) lastSeenAt DateTime @default(now()) missingScanCount Int @default(0) expiredAt DateTime? rawPayloadJson String? // Preservation of original raw source payload datePosted DateTime? companyId String? companyRef Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) postedById String? postedBy User? @relation("PostedJobs", fields: [postedById], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt interactions UserJobInteraction[] applications Application[] savedBy SavedJob[] @@index([companyId]) @@index([postedById]) @@index([location]) @@index([isRemote]) @@index([department]) @@index([experienceLevel]) @@index([datePosted]) @@index([source]) @@index([moderationStatus]) @@index([lifecycleStatus]) @@index([fingerprintHash]) @@index([sourceId]) } model UserJobInteraction { id String @id @default(cuid()) userId String jobId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) status String notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, jobId]) @@index([userId]) @@index([jobId]) } model Application { id String @id @default(cuid()) jobId String applicantId String job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) applicant User @relation(fields: [applicantId], references: [id], onDelete: Cascade) resumeId String? coverLetter String? status String @default("APPLIED") // APPLIED, SCREENING, INTERVIEW, OFFER, REJECTED, HIRED answersJson String? snapshotJson String? employerNotes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt statusHistory ApplicationStatusHistory[] candidateNotes CandidateNote[] candidateTags CandidateTag[] @@unique([jobId, applicantId]) @@index([applicantId]) @@index([jobId]) @@index([status]) } model JobAlert { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) titleQuery String? locationQuery String? isRemoteOnly Boolean @default(false) minSalary Float? frequency String @default("DAILY") // DAILY, WEEKLY isActive Boolean @default(true) lastSentAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) } model Company { id String @id @default(cuid()) name String @unique logoUrl String? website String? cultureInfo String? location String? description String? verificationStatus String @default("UNCLAIMED") // UNCLAIMED, PENDING_VERIFICATION, VERIFIED, REJECTED, REVOKED trustStatus String @default("UNVERIFIED") // NEW, UNVERIFIED, PENDING_REVIEW, VERIFIED, RESTRICTED, SUSPENDED verifiedAt DateTime? reviews CompanyReview[] members User[] orgMembers OrganizationMembership[] invitations OrganizationInvitation[] claims CompanyClaim[] jobs Job[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@index([trustStatus]) @@index([verificationStatus]) } model OrganizationMembership { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) companyId String company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) role String @default("RECRUITER") // OWNER, ADMIN, RECRUITER, HIRING_MANAGER status String @default("ACTIVE") // ACTIVE, SUSPENDED, INVITED createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, companyId]) @@index([companyId]) @@index([userId]) @@index([role]) @@map("organization_memberships") } model CompanyClaim { id String @id @default(cuid()) companyId String company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) claimantId String claimant User @relation("Claimant", fields: [claimantId], references: [id], onDelete: Cascade) corporateEmail String evidenceType String // DOMAIN_MATCH, DOCUMENT_SUBMISSION, MANUAL_REVIEW evidenceData String? // Documentation, notes, domain details status String @default("PENDING") // PENDING, UNDER_REVIEW, APPROVED, REJECTED, REVOKED adminNotes String? reviewedAt DateTime? reviewedById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([companyId]) @@index([claimantId]) @@index([status]) @@map("company_claims") } model OrganizationInvitation { id String @id @default(cuid()) companyId String company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) email String role String @default("RECRUITER") // ADMIN, RECRUITER, HIRING_MANAGER token String @unique tokenHash String? @unique invitedById String invitedBy User @relation("InvitedBy", fields: [invitedById], references: [id], onDelete: Cascade) expiresAt DateTime acceptedAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) @@index([companyId]) @@index([email]) @@index([token]) @@index([tokenHash]) @@map("organization_invitations") } model CompanyReview { id String @id @default(cuid()) companyId String company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) authorId String? author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) rating Int title String content String status String @default("PENDING") // PENDING, APPROVED, REJECTED isApproved Boolean @default(false) isReported Boolean @default(false) reportReason String? createdAt DateTime @default(now()) @@unique([companyId, authorId]) @@index([companyId]) @@index([status]) } model FeaturedJob { id String @id @default(cuid()) jobId String @unique tier String @default("FEATURED") expiresAt DateTime? createdAt DateTime @default(now()) } model SavedJob { id String @id @default(cuid()) userId String jobId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) savedAt DateTime @default(now()) @@unique([userId, jobId]) @@index([userId]) @@index([jobId]) @@map("saved_jobs") } model SavedSearch { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) name String? keywords String? location String? department String? isRemoteOnly Boolean @default(false) minSalary Float? alertFrequency String @default("DAILY") // DAILY, WEEKLY, OFF isActive Boolean @default(true) lastSentAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([isActive, alertFrequency]) @@index([createdAt]) @@map("saved_searches") } model ApplicationStatusHistory { id String @id @default(cuid()) applicationId String application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade) fromStatus String? toStatus String changedById String? changedBy User? @relation(fields: [changedById], references: [id], onDelete: SetNull) note String? createdAt DateTime @default(now()) @@index([applicationId]) @@map("application_status_history") } model CandidateNote { id String @id @default(cuid()) applicationId String application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) noteText String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([applicationId]) @@map("candidate_notes") } model CandidateTag { id String @id @default(cuid()) applicationId String application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade) tag String createdById String createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([applicationId, tag]) @@index([applicationId]) @@map("candidate_tags") } model Notification { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) type String // STATUS_CHANGE, JOB_ALERT, SYSTEM_NOTICE title String message String link String? isRead Boolean @default(false) readAt DateTime? createdAt DateTime @default(now()) @@index([userId]) @@index([userId, isRead]) @@map("notifications") } model NotificationPreference { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) emailStatusChange Boolean @default(true) emailJobAlerts Boolean @default(true) inAppStatusChange Boolean @default(true) inAppJobAlerts Boolean @default(true) digestFrequency String @default("IMMEDIATE") // IMMEDIATE, DAILY, OFF updatedAt DateTime @updatedAt @@map("notification_preferences") } model VerificationToken { id String @id @default(cuid()) identifier String token String @unique expiresAt DateTime createdAt DateTime @default(now()) @@unique([identifier, token]) @@map("verification_tokens") } model PasswordResetToken { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) token String @unique expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) @@index([userId]) @@map("password_reset_tokens") } model AuditLog { id String @id @default(cuid()) actorId String? actor User? @relation("ActorAuditLogs", fields: [actorId], references: [id], onDelete: SetNull) action String // USER_SUSPEND, USER_REACTIVATE, USER_DELETE, ROLE_CHANGE, JOB_CREATE, JOB_DELETE, REVIEW_APPROVE, REVIEW_REJECT targetId String? details String? ipAddress String? createdAt DateTime @default(now()) @@index([actorId]) @@index([action]) @@index([createdAt]) @@map("audit_logs") } model JobAnalysisCache { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) targetType String // JOB_SEEKER_MATCH, EMPLOYER_CANDIDATE_MATCH, JOB_RECOMMENDATION targetId String // jobId or candidateId contentHash String // sha256 of candidate profile + job requirements analysisVersion String @default("v1.0") modelId String @default("intelligence-hybrid-v1") score Int headline String strongMatches String // JSON array of strings potentialGaps String // JSON array of strings relevantExp String // JSON array of strings explanation String processingMs Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, targetType, targetId, contentHash]) @@index([userId, targetType]) @@index([targetId]) @@index([contentHash]) @@map("job_analysis_cache") } model JobSource { id String @id @default(cuid()) name String type String // ATS, API, RSS, CAREER_PAGE, AGGREGATOR provider String // greenhouse, lever, ashby, jobaps, worktable baseUrl String identifier String? // board slug or external id enabled Boolean @default(true) status String @default("ACTIVE") // ACTIVE, DEGRADED, FAILED, DISABLED, MAINTENANCE scheduleTier String @default("STANDARD") // HIGH_PRIORITY (6h), STANDARD (24h), LOW_PRIORITY (3d) rateLimitRpm Int @default(60) lastRunAt DateTime? lastSuccessAt DateTime? lastFailureAt DateTime? consecutiveFailures Int @default(0) averageDurationMs Int @default(0) jobsDiscoveredLastRun Int @default(0) jobsCreatedLastRun Int @default(0) jobsUpdatedLastRun Int @default(0) jobsExpiredLastRun Int @default(0) lastError String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt executionLogs SourceExecutionLog[] @@unique([provider, identifier]) @@index([status]) @@index([type]) @@index([scheduleTier]) @@map("job_sources") } model SourceExecutionLog { id String @id @default(cuid()) sourceId String source JobSource @relation(fields: [sourceId], references: [id], onDelete: Cascade) status String // SUCCESS, DEGRADED, FAILED startedAt DateTime @default(now()) completedAt DateTime? durationMs Int @default(0) discoveredCount Int @default(0) createdCount Int @default(0) updatedCount Int @default(0) expiredCount Int @default(0) duplicateCount Int @default(0) errorMessage String? diagnosticMetadata String? // JSON string of headers, status codes, payload sizes createdAt DateTime @default(now()) @@index([sourceId]) @@index([status]) @@index([createdAt]) @@map("source_execution_logs") } model BetaFeedback { id String @id @default(cuid()) userId String? userEmail String? category String // BUG, UI_PROBLEM, SEARCH_PROBLEM, JOB_DATA_PROBLEM, FEATURE_REQUEST, OTHER description String expectedBehavior String? actualBehavior String? route String? pageUrl String? viewport String? // mobile, tablet, desktop browserInfo String? createdAt DateTime @default(now()) @@index([category]) @@index([createdAt]) @@map("beta_feedback") } model BetaInvitation { id String @id @default(cuid()) email String @unique tokenHash String @unique invitedBy String? role String @default("BETA_TESTER") // BETA_TESTER, EMPLOYER, ADMIN acceptedAt DateTime? lastActiveAt DateTime? expiresAt DateTime createdAt DateTime @default(now()) @@index([email]) @@index([tokenHash]) @@map("beta_invitations") }