feat(db): Migrate Student endpoints to Prisma
This commit is contained in:
parent
320b627a91
commit
22ecf41e9d
350
server-prisma.ts
350
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue