advance
This commit is contained in:
215
prisma/schema.prisma
Executable file
215
prisma/schema.prisma
Executable file
@@ -0,0 +1,215 @@
|
||||
// prisma/schema.prisma
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// --- ENUMS ---
|
||||
|
||||
enum UserRole {
|
||||
SUPER_ADMIN // You (Full Access)
|
||||
ORG_ADMIN // Law Firm Manager (View Team Progress, Manage Seats)
|
||||
TEACHER // Content Creator (Create/Edit their own courses)
|
||||
LEARNER // Student (View content, take quizzes)
|
||||
}
|
||||
|
||||
enum ContentStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum ProficiencyLevel {
|
||||
BEGINNER // A1-A2
|
||||
INTERMEDIATE // B1-B2
|
||||
ADVANCED // C1-C2
|
||||
EXPERT // Legal Specific
|
||||
}
|
||||
|
||||
enum ExerciseType {
|
||||
MULTIPLE_CHOICE
|
||||
FILL_IN_BLANK
|
||||
TRANSLATION
|
||||
MATCHING
|
||||
}
|
||||
|
||||
// --- MODELS ---
|
||||
|
||||
model Profile {
|
||||
id String @id @default(uuid()) // Links to Supabase Auth.uid()
|
||||
email String @unique
|
||||
fullName String?
|
||||
avatarUrl String?
|
||||
role UserRole @default(LEARNER)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relationships
|
||||
memberships Membership[] // B2B Access
|
||||
enrollments Enrollment[] // B2C Purchases (Mercado Pago)
|
||||
progress UserProgress[]
|
||||
certificates Certificate[]
|
||||
authoredCourses Course[] @relation("CourseAuthor") // Teachers own courses
|
||||
}
|
||||
|
||||
model Company {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
logoUrl String?
|
||||
billingEmail String?
|
||||
maxSeats Int @default(5) // Limit for B2B plans
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships Membership[]
|
||||
certificates Certificate[]
|
||||
}
|
||||
|
||||
model Membership {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
companyId String
|
||||
isActive Boolean @default(true)
|
||||
role String @default("MEMBER") // Internal org role (Member/Admin)
|
||||
|
||||
joinedAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user Profile @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, companyId]) // Prevent duplicate memberships
|
||||
}
|
||||
|
||||
model Course {
|
||||
id String @id @default(uuid())
|
||||
title Json // { "en": "...", "es": "..." }
|
||||
slug String @unique // Required for SEO URLs (e.g. /course/contract-law)
|
||||
description Json // { "en": "...", "es": "..." }
|
||||
level ProficiencyLevel @default(INTERMEDIATE)
|
||||
tags String[]
|
||||
|
||||
status ContentStatus @default(DRAFT)
|
||||
price Decimal @default(0.00) // Price in MXN
|
||||
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Prerequisites
|
||||
prerequisiteId String?
|
||||
prerequisite Course? @relation("CoursePrerequisites", fields: [prerequisiteId], references: [id])
|
||||
dependents Course[] @relation("CoursePrerequisites")
|
||||
|
||||
author Profile @relation("CourseAuthor", fields: [authorId], references: [id])
|
||||
modules Module[]
|
||||
certificates Certificate[]
|
||||
enrollments Enrollment[]
|
||||
}
|
||||
|
||||
model Enrollment {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
courseId String
|
||||
|
||||
// Payment Proof (Mercado Pago)
|
||||
amountPaid Decimal // e.g. 500.00
|
||||
currency String @default("MXN")
|
||||
paymentMethod String @default("MERCADO_PAGO")
|
||||
externalId String? // The Mercado Pago "payment_id" or "preference_id"
|
||||
|
||||
purchasedAt DateTime @default(now())
|
||||
|
||||
user Profile @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, courseId]) // Prevent buying the same course twice
|
||||
}
|
||||
|
||||
model Module {
|
||||
id String @id @default(uuid())
|
||||
courseId String
|
||||
title Json
|
||||
orderIndex Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||
lessons Lesson[]
|
||||
}
|
||||
|
||||
model Lesson {
|
||||
id String @id @default(uuid())
|
||||
moduleId String
|
||||
title Json
|
||||
description Json?
|
||||
slug String? // Optional for direct linking
|
||||
orderIndex Int
|
||||
videoUrl String?
|
||||
estimatedDuration Int // Seconds
|
||||
version Int @default(1)
|
||||
isFreePreview Boolean @default(false) // Marketing hook
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||
exercises Exercise[]
|
||||
resources Resource[]
|
||||
userProgress UserProgress[]
|
||||
}
|
||||
|
||||
model Exercise {
|
||||
id String @id @default(uuid())
|
||||
lessonId String
|
||||
type ExerciseType
|
||||
content Json // { question: "...", options: [...], answer: "..." }
|
||||
orderIndex Int @default(0)
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Resource {
|
||||
id String @id @default(uuid())
|
||||
lessonId String
|
||||
fileUrl String
|
||||
displayName Json
|
||||
fileType String
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model UserProgress {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
lessonId String
|
||||
isCompleted Boolean @default(false)
|
||||
score Int? // For quiz results
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
lastPlayedAt DateTime @default(now()) // For "Resume" feature
|
||||
|
||||
user Profile @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, lessonId]) // One progress record per lesson
|
||||
}
|
||||
|
||||
model Certificate {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
courseId String
|
||||
companyId String? // Captured for co-branding (Nullable for B2C)
|
||||
issuedAt DateTime @default(now())
|
||||
metadataSnapshot Json // Burn the course name/version here
|
||||
|
||||
user Profile @relation(fields: [userId], references: [id])
|
||||
course Course @relation(fields: [courseId], references: [id])
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
}
|
||||
107
prisma/seed.ts
Executable file
107
prisma/seed.ts
Executable file
@@ -0,0 +1,107 @@
|
||||
// prisma/seed.ts
|
||||
import { PrismaClient, UserRole, ContentStatus, ProficiencyLevel, ExerciseType } from '@prisma/client'
|
||||
import { Pool } from 'pg'
|
||||
import { PrismaPg } from '@prisma/adapter-pg'
|
||||
|
||||
const connectionString = process.env.DIRECT_URL
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting seed...')
|
||||
|
||||
// 1. Setup the Adapter (Crucial for Prisma 7 + Serverless)
|
||||
const pool = new Pool({ connectionString })
|
||||
const adapter = new PrismaPg(pool)
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
try {
|
||||
// 2. Create a Teacher/Super Admin
|
||||
const teacher = await prisma.profile.upsert({
|
||||
where: { email: 'admin@acve.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@acve.com',
|
||||
fullName: 'ACVE Admin',
|
||||
// ✅ CORRECT: Uses the strict Enum value
|
||||
role: UserRole.SUPER_ADMIN,
|
||||
avatarUrl: 'https://github.com/shadcn.png',
|
||||
},
|
||||
})
|
||||
console.log(`👤 Created User: ${teacher.email}`)
|
||||
|
||||
// 3. Create a Law Firm (B2B)
|
||||
const lawFirm = await prisma.company.create({
|
||||
data: {
|
||||
name: 'García & Partners',
|
||||
billingEmail: 'billing@garcia.com',
|
||||
maxSeats: 10,
|
||||
memberships: {
|
||||
create: {
|
||||
userId: teacher.id,
|
||||
// ✅ FIXED: Use 'ORG_ADMIN' to match your UserRole terminology
|
||||
// Since this field is a String in schema, we use the string value explicitly.
|
||||
role: 'ORG_ADMIN',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
console.log(`🏢 Created Law Firm: ${lawFirm.name}`)
|
||||
|
||||
// 4. Create the Bilingual Course (Contract Law)
|
||||
const course = await prisma.course.create({
|
||||
data: {
|
||||
title: {
|
||||
en: "Legal English: Contract Basics",
|
||||
es: "Inglés Jurídico: Fundamentos de Contratos"
|
||||
},
|
||||
slug: "contract-law-101",
|
||||
description: {
|
||||
en: "Master the terminology of international contracts.",
|
||||
es: "Domina la terminología de contratos internacionales."
|
||||
},
|
||||
level: ProficiencyLevel.INTERMEDIATE,
|
||||
status: ContentStatus.PUBLISHED,
|
||||
price: 499.00,
|
||||
authorId: teacher.id,
|
||||
tags: ["contracts", "civil law", "compliance"],
|
||||
|
||||
modules: {
|
||||
create: {
|
||||
title: { en: "Module 1: Offer and Acceptance", es: "Módulo 1: Oferta y Aceptación" },
|
||||
orderIndex: 1,
|
||||
lessons: {
|
||||
create: {
|
||||
title: { en: "The Elements of a Contract", es: "Los Elementos de un Contrato" },
|
||||
slug: "elements-of-contract",
|
||||
orderIndex: 1,
|
||||
estimatedDuration: 600,
|
||||
isFreePreview: true,
|
||||
exercises: {
|
||||
create: {
|
||||
type: ExerciseType.MULTIPLE_CHOICE,
|
||||
orderIndex: 1,
|
||||
content: {
|
||||
question: "What is 'Consideration' in a contract?",
|
||||
options: ["Payment", "Thoughtfulness", "A value exchange", "A signature"],
|
||||
correctAnswer: "A value exchange"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
console.log(`📚 Created Course: ${course.slug}`)
|
||||
|
||||
console.log('✅ Seed complete!')
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user