feat(db): Migrate Student endpoints to Prisma

This commit is contained in:
Sidney Gomes 2026-07-22 21:21:17 -03:00
parent 320b627a91
commit 22ecf41e9d
1 changed files with 201 additions and 149 deletions

View File

@ -234,25 +234,24 @@ app.get('/api/auth/me', authenticateToken, async (req: AuthenticatedRequest, res
});
// 3.1. Profile: Update Data
app.put('/api/users/profile', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const { name, whatsapp, birthDate, cpf } = req.body;
const userId = req.user!.id;
const db = loadDb();
const user = db.users.find(u => u.id === userId);
if (!user) {
try {
const user = await prisma.user.update({
where: { id: userId },
data: {
...(name !== undefined && { name }),
...(whatsapp !== undefined && { whatsapp }),
...(birthDate !== undefined && { birthDate }),
...(cpf !== undefined && { cpf })
}
});
res.json({ success: true, user });
} catch (error) {
res.status(404).json({ error: 'Usuário não encontrado' });
return;
}
if (name !== undefined) user.name = name;
if (whatsapp !== undefined) user.whatsapp = whatsapp;
if (birthDate !== undefined) user.birthDate = birthDate;
if (cpf !== undefined) user.cpf = cpf;
saveDb(db);
res.json({ success: true, user });
});
// 3.2. Profile: Avatar Upload
@ -277,12 +276,10 @@ app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async
// URL Pública configurada via Traefik
const avatarUrl = `https://s3-estudo.microtecinformaticacurso.com.br/microtecflix/${filename}`;
const db = loadDb();
const user = db.users.find(u => u.id === userId);
if (user) {
user.avatarUrl = avatarUrl;
saveDb(db);
}
await prisma.user.update({
where: { id: userId },
data: { avatarUrl }
});
res.json({ success: true, avatarUrl });
} catch (err: any) {
@ -292,33 +289,51 @@ app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async
});
// 4. Courses List (With Progress)
app.get('/api/courses', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
const db = loadDb();
app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.id;
const user = db.users.find(u => u.id === userId);
const user = await prisma.user.findUnique({
where: { id: userId }
});
const unlockedCourses = user?.unlockedCourses || [];
const isAdmin = user?.role === 'admin';
// Enhance courses with overall user progress
const coursesWithProgress = db.courses.map(course => {
// Find all lessons in this course
const courseModules = db.modules.filter(m => m.courseId === course.id);
const courseModuleIds = courseModules.map(m => m.id);
const courseLessons = db.lessons.filter(l => courseModuleIds.includes(l.moduleId));
const courses = await prisma.course.findMany({
include: {
modules: {
include: {
lessons: true
}
}
}
});
const userProgress = await prisma.progress.findMany({
where: { userId, completed: true }
});
const userProgressLessonIds = userProgress.map(p => p.lessonId);
const coursesWithProgress = courses.map(course => {
const courseLessons = course.modules.flatMap(m => m.lessons);
const totalLessons = courseLessons.length;
let completedLessons = 0;
if (totalLessons > 0) {
const userProgress = db.progress.filter(p => p.userId === userId && p.completed);
const userProgressLessonIds = userProgress.map(p => p.lessonId);
completedLessons = courseLessons.filter(l => userProgressLessonIds.includes(l.id)).length;
}
const progressPercentage = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
return {
...course,
id: course.id,
title: course.title,
description: course.description,
thumbnailUrl: course.thumbnailUrl,
videoUrl: course.videoUrl,
isLocked: course.isLocked,
price: course.price,
category: course.category,
order: course.order,
totalLessons,
completedLessons,
progress: progressPercentage,
@ -330,45 +345,58 @@ app.get('/api/courses', authenticateToken, (req: AuthenticatedRequest, res: Resp
});
// 5. Course Details (with Modules, Lessons, Progress & Notes)
app.get('/api/courses/:id', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
const db = loadDb();
app.get('/api/courses/:id', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.id;
const courseId = req.params.id;
const course = db.courses.find(c => c.id === courseId);
const course = await prisma.course.findUnique({
where: { id: courseId },
include: {
modules: {
orderBy: { order: 'asc' },
include: {
lessons: {
orderBy: { order: 'asc' }
}
}
}
}
});
if (!course) {
res.status(404).json({ error: 'Curso não encontrado' });
return;
}
const user = db.users.find(u => u.id === userId);
const user = await prisma.user.findUnique({
where: { id: userId }
});
const unlockedCourses = user?.unlockedCourses || [];
const isAdmin = user?.role === 'admin';
const isUnlocked = !course.isLocked || unlockedCourses.includes(course.id) || isAdmin;
// Get modules
const courseModules = db.modules
.filter(m => m.courseId === courseId)
.sort((a, b) => a.order - b.order);
const progressRecords = await prisma.progress.findMany({
where: { userId, lesson: { moduleId: { in: course.modules.map(m => m.id) } } }
});
const noteRecords = await prisma.note.findMany({
where: { userId, lesson: { moduleId: { in: course.modules.map(m => m.id) } } }
});
// Get lessons and enhance with progress
const modulesWithLessons = courseModules.map(mod => {
const lessons = db.lessons
.filter(l => l.moduleId === mod.id)
.sort((a, b) => a.order - b.order)
.map(lesson => {
const prog = db.progress.find(p => p.userId === userId && p.lessonId === lesson.id);
const note = db.notes.find(n => n.userId === userId && n.lessonId === lesson.id);
const modulesWithLessons = course.modules.map(mod => {
const lessons = mod.lessons.map(lesson => {
const prog = progressRecords.find(p => p.lessonId === lesson.id);
const note = noteRecords.find(n => n.lessonId === lesson.id);
return {
...lesson,
progress: prog ? {
watchedPercentage: prog.watchedPercentage,
completed: prog.completed
} : { watchedPercentage: 0, completed: false },
note: note ? note.content : ''
};
});
return {
...lesson,
progress: prog ? {
watchedPercentage: prog.watchedPercentage,
completed: prog.completed
} : { watchedPercentage: 0, completed: false },
note: note ? note.content : ''
};
});
return {
...mod,
@ -376,9 +404,10 @@ app.get('/api/courses/:id', authenticateToken, (req: AuthenticatedRequest, res:
};
});
const { modules, ...courseData } = course;
res.json({
course: {
...course,
...courseData,
isUnlocked
},
modules: modulesWithLessons
@ -416,9 +445,8 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn
const userId = req.user!.id;
const { billingType, creditCardInfo, installmentCount } = req.body;
const db = loadDb();
const user = db.users.find(u => u.id === userId);
const course = db.courses.find(c => c.id === courseId);
const user = await prisma.user.findUnique({ where: { id: userId } });
const course = await prisma.course.findUnique({ where: { id: courseId } });
if (!user) {
res.status(404).json({ error: 'Usuário não encontrado' });
@ -437,6 +465,10 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn
}
if (!customerId) {
customerId = await AsaasService.createCustomer(user.name, user.email, user.cpf);
await prisma.user.update({
where: { id: userId },
data: { asaasCustomerId: customerId }
});
user.asaasCustomerId = customerId;
}
@ -475,20 +507,18 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn
pixData = await AsaasService.getPixQrCode(asaasPayment.id);
}
const newPayment: SubscriptionPayment = {
id: asaasPayment.id,
userId,
value: course.price || 49.90,
status: 'PENDING',
billingType: billingType || 'PIX',
invoiceUrl: asaasPayment.invoiceUrl,
dueDate: asaasPayment.dueDate || new Date().toISOString().split('T')[0],
paidAt: null,
createdAt: new Date().toISOString(),
courseId: courseId
};
db.payments.push(newPayment);
saveDb(db);
const newPayment = await prisma.subscriptionPayment.create({
data: {
id: asaasPayment.id,
userId: userId,
value: course.price || 49.90,
status: 'PENDING',
billingType: billingType || 'PIX',
invoiceUrl: asaasPayment.invoiceUrl,
dueDate: asaasPayment.dueDate ? new Date(asaasPayment.dueDate) : new Date(),
courseId: courseId
}
});
res.json({ success: true, payment: newPayment, pixData });
} catch (err: any) {
@ -498,7 +528,7 @@ app.post('/api/courses/:id/unlock', authenticateToken, checkoutRateLimiter, asyn
});
// 6. Lesson Progress tracking
app.post('/api/lessons/:id/progress', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
app.post('/api/lessons/:id/progress', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const { watchedPercentage } = req.body;
const lessonId = req.params.id;
const userId = req.user!.id;
@ -508,87 +538,99 @@ app.post('/api/lessons/:id/progress', authenticateToken, (req: AuthenticatedRequ
return;
}
const db = loadDb();
const lesson = db.lessons.find(l => l.id === lessonId);
const lesson = await prisma.lesson.findUnique({ where: { id: lessonId } });
if (!lesson) {
res.status(404).json({ error: 'Aula não encontrada' });
return;
}
let progIdx = db.progress.findIndex(p => p.userId === userId && p.lessonId === lessonId);
const isCompleted = watchedPercentage >= 90;
if (progIdx >= 0) {
db.progress[progIdx].watchedPercentage = Math.max(db.progress[progIdx].watchedPercentage, watchedPercentage);
if (isCompleted) {
db.progress[progIdx].completed = true;
}
db.progress[progIdx].updatedAt = new Date().toISOString();
} else {
db.progress.push({
id: `prog_${Math.random().toString(36).substring(2, 9)}`,
userId,
lessonId,
watchedPercentage,
completed: isCompleted,
updatedAt: new Date().toISOString()
try {
const existingProgress = await prisma.progress.findFirst({
where: { userId, lessonId }
});
}
saveDb(db);
res.json({ success: true, completed: isCompleted });
if (existingProgress) {
await prisma.progress.update({
where: { id: existingProgress.id },
data: {
watchedPercentage: Math.max(existingProgress.watchedPercentage, watchedPercentage),
completed: isCompleted ? true : existingProgress.completed
}
});
} else {
await prisma.progress.create({
data: {
userId,
lessonId,
watchedPercentage,
completed: isCompleted
}
});
}
res.json({ success: true, completed: isCompleted });
} catch (err: any) {
res.status(500).json({ error: 'Erro ao salvar progresso' });
}
});
// 7. Lesson Notes and Doubts
app.get('/api/lessons/:id/notes', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
app.get('/api/lessons/:id/notes', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const lessonId = req.params.id;
const userId = req.user!.id;
const db = loadDb();
const note = db.notes.find(n => n.userId === userId && n.lessonId === lessonId);
const note = await prisma.note.findFirst({
where: { userId, lessonId }
});
res.json({ content: note ? note.content : '' });
});
app.post('/api/lessons/:id/notes', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
app.post('/api/lessons/:id/notes', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const { content } = req.body;
const lessonId = req.params.id;
const userId = req.user!.id;
const db = loadDb();
let noteIdx = db.notes.findIndex(n => n.userId === userId && n.lessonId === lessonId);
if (noteIdx >= 0) {
db.notes[noteIdx].content = content;
db.notes[noteIdx].createdAt = new Date().toISOString();
} else {
db.notes.push({
id: `note_${Math.random().toString(36).substring(2, 9)}`,
userId,
lessonId,
content,
createdAt: new Date().toISOString()
try {
const existingNote = await prisma.note.findFirst({
where: { userId, lessonId }
});
}
saveDb(db);
res.json({ success: true });
if (existingNote) {
await prisma.note.update({
where: { id: existingNote.id },
data: { content }
});
} else {
await prisma.note.create({
data: {
userId,
lessonId,
content
}
});
}
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: 'Erro ao salvar anotação' });
}
});
// 8. Subscription status & Invoice history
app.get('/api/subscription/status', authenticateToken, (req: AuthenticatedRequest, res: Response) => {
const db = loadDb();
app.get('/api/subscription/status', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.id;
const user = db.users.find(u => u.id === userId);
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
res.status(404).json({ error: 'Usuário não encontrado' });
return;
}
const payments = db.payments
.filter(p => p.userId === userId)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const payments = await prisma.subscriptionPayment.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
});
res.json({
status: user.subscriptionStatus,
@ -607,8 +649,7 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
return;
}
const db = loadDb();
const user = db.users.find(u => u.id === userId);
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
res.status(404).json({ error: 'Usuário não encontrado' });
return;
@ -622,22 +663,25 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
}
if (!customerId) {
customerId = await AsaasService.createCustomer(user.name, user.email, user.cpf);
await prisma.user.update({
where: { id: userId },
data: { asaasCustomerId: customerId }
});
user.asaasCustomerId = customerId;
}
let plan = planId ? db.plans?.find(p => p.id === planId) : null;
let plan = planId ? await prisma.plan.findUnique({ where: { id: planId } }) : null;
let price = plan ? plan.price : 49.90;
let cycle = plan ? plan.cycle : 'MONTHLY';
let description = plan ? `Plano: ${plan.title}` : 'Assinatura Mensal DevFlix';
let asaasPaymentId = '';
let invoiceUrlStr = '';
let statusAsaas: 'PENDING' | 'CONFIRMED' = billingType === 'CREDIT_CARD' ? 'CONFIRMED' : 'PENDING';
let statusAsaas = billingType === 'CREDIT_CARD' ? 'CONFIRMED' : 'PENDING';
let asaasDueDate = new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().split('T')[0];
let pixData = null;
if (cycle === 'LIFETIME') {
// Pagamento Único
let processedCardInfo = creditCardInfo;
if (billingType === 'CREDIT_CARD' && creditCardInfo?.creditCard?.number) {
try {
@ -672,7 +716,6 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
pixData = await AsaasService.getPixQrCode(asaasPayment.id);
}
} else {
// Assinatura (Mensal ou Anual)
const { subscriptionId, payment } = await AsaasService.createSubscription(
customerId,
price,
@ -687,36 +730,45 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
asaasDueDate = payment.dueDate || asaasDueDate;
}
// Add subscription payment to database
const newPayment: SubscriptionPayment = {
id: asaasPaymentId,
userId,
value: price,
status: statusAsaas,
billingType,
invoiceUrl: invoiceUrlStr,
dueDate: asaasDueDate,
paidAt: statusAsaas === 'CONFIRMED' ? new Date().toISOString() : null,
createdAt: new Date().toISOString(),
planId: planId
};
db.payments.push(newPayment);
const newPayment = await prisma.subscriptionPayment.create({
data: {
id: asaasPaymentId,
userId: userId,
value: price,
status: statusAsaas,
billingType: billingType,
invoiceUrl: invoiceUrlStr,
dueDate: new Date(asaasDueDate),
paidAt: statusAsaas === 'CONFIRMED' ? new Date() : null,
planId: planId
}
});
// If sandbox auto-approves credit card
if (statusAsaas === 'CONFIRMED') {
if (cycle === 'LIFETIME' && plan) {
if (!plan.includedCourses || plan.includedCourses.length === 0) {
user.subscriptionStatus = 'ACTIVE'; // Libera tudo
await prisma.user.update({
where: { id: userId },
data: { subscriptionStatus: 'ACTIVE' }
});
} else {
user.unlockedCourses = [...(user.unlockedCourses || []), ...plan.includedCourses];
await prisma.user.update({
where: { id: userId },
data: {
unlockedCourses: {
set: [...new Set([...(user.unlockedCourses || []), ...plan.includedCourses])]
}
}
});
}
} else {
user.subscriptionStatus = 'ACTIVE';
await prisma.user.update({
where: { id: userId },
data: { subscriptionStatus: 'ACTIVE' }
});
}
}
saveDb(db);
res.json({ success: true, payment: newPayment, pixData });
} catch (err: any) {
console.error('Subscription error:', err);