From 22ecf41e9d1241d0fc0a9e6b3cbdc9f88b167979 Mon Sep 17 00:00:00 2001 From: Sidney Gomes Date: Wed, 22 Jul 2026 21:21:17 -0300 Subject: [PATCH] feat(db): Migrate Student endpoints to Prisma --- server-prisma.ts | 350 +++++++++++++++++++++++++++-------------------- 1 file changed, 201 insertions(+), 149 deletions(-) diff --git a/server-prisma.ts b/server-prisma.ts index 604f879..6436931 100644 --- a/server-prisma.ts +++ b/server-prisma.ts @@ -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);