JB/web/prisma/schema.prisma

595 lines
20 KiB
Text

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")
}