This commit is contained in:
Marcelo
2026-02-17 00:07:00 +00:00
parent b7a86a2d1c
commit be4ca2ed78
92 changed files with 6850 additions and 1188 deletions

215
prisma/schema.prisma Executable file
View 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
View 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()