• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

classconnect-grupo3 / courses-service / 15914402693

26 Jun 2025 11:12PM UTC coverage: 81.843% (-5.9%) from 87.722%
15914402693

Pull #49

github

web-flow
Merge pull request #48 from classconnect-grupo3/notification-messages-and-testing

Notification messages and testing
Pull Request #49: Final merge (thank you for the ride)

1542 of 2153 new or added lines in 24 files covered. (71.62%)

16 existing lines in 5 files now uncovered.

4548 of 5557 relevant lines covered (81.84%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.23
/src/controller/enrollment_controller.go
1
package controller
2

3
import (
4
        "courses-service/src/ai"
5
        "courses-service/src/model"
6
        "courses-service/src/queues"
7
        "courses-service/src/schemas"
8
        "courses-service/src/service"
9
        "fmt"
10
        "log/slog"
11
        "net/http"
12
        "slices"
13
        "time"
14

15
        "github.com/gin-gonic/gin"
16
)
17

18
type EnrollmentController struct {
19
        enrollmentService  service.EnrollmentServiceInterface
20
        aiClient           *ai.AiClient
21
        activityService    service.TeacherActivityServiceInterface
22
        notificationsQueue queues.NotificationsQueueInterface
23
}
24

25
func NewEnrollmentController(enrollmentService service.EnrollmentServiceInterface, aiClient *ai.AiClient, activityService service.TeacherActivityServiceInterface, notificationsQueue queues.NotificationsQueueInterface) *EnrollmentController {
1✔
26
        return &EnrollmentController{
1✔
27
                enrollmentService:  enrollmentService,
1✔
28
                aiClient:           aiClient,
1✔
29
                activityService:    activityService,
1✔
30
                notificationsQueue: notificationsQueue,
1✔
31
        }
1✔
32
}
1✔
33

34
// @Summary Enroll a student in a course
35
// @Description Enroll a student in a course
36
// @Tags enrollments
37
// @Accept json
38
// @Produce json
39
// @Param id path string true "Course ID"
40
// @Param enrollmentRequest body schemas.EnrollStudentRequest true "Enrollment request"
41
// @Router /courses/{id}/enroll [post]
42
func (c *EnrollmentController) EnrollStudent(ctx *gin.Context) {
1✔
43
        slog.Debug("Enrolling student", "studentId", ctx.Param("studentId"), "courseId", ctx.Param("id"))
1✔
44
        courseID := ctx.Param("id")
1✔
45

1✔
46
        if courseID == "" {
2✔
47
                slog.Error("Invalid course ID", "courseId", courseID)
1✔
48
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid course ID"})
1✔
49
                return
1✔
50
        }
1✔
51

52
        var enrollmentRequest schemas.EnrollStudentRequest
1✔
53
        if err := ctx.ShouldBindJSON(&enrollmentRequest); err != nil {
2✔
54
                slog.Error("Error binding enrollment request", "error", err)
1✔
55
                ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1✔
56
                return
1✔
57
        }
1✔
58

59
        err := c.enrollmentService.EnrollStudent(enrollmentRequest.StudentID, courseID)
1✔
60
        if err != nil {
2✔
61
                slog.Error("Error enrolling student", "error", err)
1✔
62
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
63
                return
1✔
64
        }
1✔
65

66
        slog.Debug("Student enrolled in course", "studentId", enrollmentRequest.StudentID, "courseId", courseID)
1✔
67

1✔
68
        message := queues.NewEnrolledStudentToCourseMessage(courseID, enrollmentRequest.StudentID)
1✔
69
        slog.Info("Publishing message", "message", message)
1✔
70
        err = c.notificationsQueue.Publish(message)
1✔
71
        if err != nil {
1✔
NEW
72
                slog.Error("Error publishing message", "error", err)
×
NEW
73
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
74
                return
×
NEW
75
        }
×
76

77
        ctx.JSON(http.StatusCreated, gin.H{"message": "Student successfully enrolled in course"})
1✔
78
}
79

80
// @Summary Unenroll a student from a course
81
// @Description Unenroll a student from a course
82
// @Tags enrollments
83
// @Accept json
84
// @Produce json
85
// @Param id path string true "Course ID"
86
// @Param studentId query string true "Student ID"
87
// @Success 200 {object} schemas.UnenrollStudentResponse
88
// @Router /courses/{id}/unenroll [delete]
89
func (c *EnrollmentController) UnenrollStudent(ctx *gin.Context) {
1✔
90
        slog.Debug("Unenrolling student", "studentId", ctx.Param("studentId"), "courseId", ctx.Param("id"))
1✔
91
        courseID := ctx.Param("id")
1✔
92

1✔
93
        if courseID == "" {
2✔
94
                slog.Error("Invalid student ID or course ID")
1✔
95
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid student ID or course ID"})
1✔
96
                return
1✔
97
        }
1✔
98

99
        studentId := ctx.Query("studentId")
1✔
100
        if studentId == "" {
2✔
101
                slog.Error("Student ID is required")
1✔
102
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Student ID is required"})
1✔
103
                return
1✔
104
        }
1✔
105

106
        err := c.enrollmentService.UnenrollStudent(studentId, courseID)
1✔
107
        if err != nil {
2✔
108
                slog.Error("Error unenrolling student", "error", err)
1✔
109
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
110
                return
1✔
111
        }
1✔
112

113
        slog.Debug("Student unenrolled from course", "studentId", studentId, "courseId", courseID)
1✔
114

1✔
115
        message := queues.NewUnenrolledStudentFromCourseMessage(courseID, studentId, "")
1✔
116
        slog.Info("Publishing message", "message", message)
1✔
117
        err = c.notificationsQueue.Publish(message)
1✔
118
        if err != nil {
1✔
NEW
119
                slog.Error("Error publishing message", "error", err)
×
NEW
120
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
121
                return
×
NEW
122
        }
×
123

124
        ctx.JSON(http.StatusOK, gin.H{"message": "Student successfully unenrolled from course"})
1✔
125
}
126

127
// @Summary Get enrollments by course ID
128
// @Description Get enrollments by course ID
129
// @Tags enrollments
130
// @Accept json
131
// @Produce json
132
// @Param id path string true "Course ID"
133
// @Success 200 {array} model.Enrollment
134
// @Router /courses/{id}/enrollments [get]
135
func (c *EnrollmentController) GetEnrollmentsByCourseId(ctx *gin.Context) {
1✔
136
        slog.Debug("Getting enrollments by course ID", "courseId", ctx.Param("id"))
1✔
137
        courseID := ctx.Param("id")
1✔
138

1✔
139
        enrollments, err := c.enrollmentService.GetEnrollmentsByCourseId(courseID)
1✔
140
        if err != nil {
2✔
141
                slog.Error("Error getting enrollments by course ID", "error", err)
1✔
142
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
143
                return
1✔
144
        }
1✔
145

146
        ctx.JSON(http.StatusOK, enrollments)
1✔
147
}
148

149
// @Summary Set a course as favourite
150
// @Description Set a course as favourite
151
// @Tags enrollments
152
// @Accept json
153
// @Produce json
154
// @Param id path string true "Course ID"
155
// @Param favouriteCourseRequest body schemas.SetFavouriteCourseRequest true "Favourite course request"
156
// @Success 200 {object} schemas.SetFavouriteCourseResponse
157
// @Router /courses/{id}/favourite [post]
158
func (c *EnrollmentController) SetFavouriteCourse(ctx *gin.Context) {
1✔
159
        slog.Debug("Setting favourite course", "courseId", ctx.Param("id"))
1✔
160
        courseID := ctx.Param("id")
1✔
161

1✔
162
        if courseID == "" {
2✔
163
                slog.Error("Invalid course ID")
1✔
164
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid course ID"})
1✔
165
                return
1✔
166
        }
1✔
167

168
        var favouriteCourseRequest schemas.SetFavouriteCourseRequest
1✔
169
        if err := ctx.ShouldBindJSON(&favouriteCourseRequest); err != nil {
2✔
170
                slog.Error("Error binding favourite course request", "error", err)
1✔
171
                ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1✔
172
                return
1✔
173
        }
1✔
174

175
        err := c.enrollmentService.SetFavouriteCourse(favouriteCourseRequest.StudentID, courseID)
1✔
176
        if err != nil {
2✔
177
                slog.Error("Error setting favourite course", "error", err)
1✔
178
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
179
                return
1✔
180
        }
1✔
181

182
        slog.Debug("Favourite course set", "studentId", favouriteCourseRequest.StudentID, "courseId", courseID)
1✔
183
        ctx.JSON(http.StatusOK, gin.H{"message": "Favourite course set"})
1✔
184
}
185

186
// @Summary Unset a course as favourite
187
// @Description Unset a course as favourite
188
// @Tags enrollments
189
// @Accept json
190
// @Produce json
191
// @Param id path string true "Course ID"
192
// @Param studentId query string true "Student ID"
193
// @Success 200 {object} schemas.UnsetFavouriteCourseResponse
194
// @Router /courses/{id}/favourite [delete]
195
func (c *EnrollmentController) UnsetFavouriteCourse(ctx *gin.Context) {
1✔
196
        slog.Debug("Unsetting favourite course", "courseId", ctx.Param("id"))
1✔
197
        courseID := ctx.Param("id")
1✔
198

1✔
199
        if courseID == "" {
2✔
200
                slog.Error("Invalid course ID")
1✔
201
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid course ID"})
1✔
202
                return
1✔
203
        }
1✔
204

205
        studentId := ctx.Query("studentId")
1✔
206
        if studentId == "" {
2✔
207
                slog.Error("Student ID is required")
1✔
208
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Student ID is required"})
1✔
209
                return
1✔
210
        }
1✔
211
        err := c.enrollmentService.UnsetFavouriteCourse(studentId, courseID)
1✔
212
        if err != nil {
2✔
213
                slog.Error("Error unsetting favourite course", "error", err)
1✔
214
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
215
                return
1✔
216
        }
1✔
217

218
        slog.Debug("Favourite course unset", "studentId", studentId, "courseId", courseID)
1✔
219
        ctx.JSON(http.StatusOK, gin.H{"message": "Favourite course unset"})
1✔
220
}
221

222
// @Summary Create a feedback for a course
223
// @Description Create a feedback for a course
224
// @Tags enrollments
225
// @Accept json
226
// @Produce json
227
// @Param id path string true "Course ID"
228
// @Param feedbackRequest body schemas.CreateStudentFeedbackRequest true "Feedback request"
229
// @Success 200 {object} model.StudentFeedback
230
// @Router /courses/{id}/student-feedback [post]
231
func (c *EnrollmentController) CreateFeedback(ctx *gin.Context) {
1✔
232
        slog.Debug("Creating feedback", "courseId", ctx.Param("id"))
1✔
233
        courseID := ctx.Param("id")
1✔
234

1✔
235
        if courseID == "" {
2✔
236
                slog.Error("Invalid course ID")
1✔
237
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid course ID"})
1✔
238
                return
1✔
239
        }
1✔
240

241
        var feedbackRequest schemas.CreateStudentFeedbackRequest
1✔
242
        if err := ctx.ShouldBindJSON(&feedbackRequest); err != nil {
2✔
243
                slog.Error("Error binding feedback request", "error", err)
1✔
244
                ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1✔
245
                return
1✔
246
        }
1✔
247

248
        if !slices.Contains(model.FeedbackTypes, feedbackRequest.FeedbackType) {
2✔
249
                slog.Error("Invalid feedback type")
1✔
250
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid feedback type"})
1✔
251
                return
1✔
252
        }
1✔
253

254
        err := c.enrollmentService.CreateStudentFeedback(feedbackRequest)
1✔
255
        if err != nil {
2✔
256
                slog.Error("Error creating feedback", "error", err)
1✔
257
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
258
                return
1✔
259
        }
1✔
260

261
        slog.Debug("Feedback created", "studentId", feedbackRequest.StudentUUID, "teacherId", feedbackRequest.TeacherUUID)
1✔
262

1✔
263
        message := queues.NewFeedbackCreatedMessage(feedbackRequest.StudentUUID, courseID, "", feedbackRequest.Feedback, feedbackRequest.Score, time.Now())
1✔
264
        slog.Info("Publishing message", "message", message)
1✔
265
        err = c.notificationsQueue.Publish(message)
1✔
266
        if err != nil {
1✔
NEW
267
                slog.Error("Error publishing message", "error", err)
×
NEW
268
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
269
                return
×
NEW
270
        }
×
271
        ctx.JSON(http.StatusOK, gin.H{"message": "Feedback created"})
1✔
272
}
273

274
// @Summary Get feedback by student ID
275
// @Description Get feedback by student ID
276
// @Tags enrollments
277
// @Accept json
278
// @Produce json
279
// @Param id path string true "Student ID"
280
// @Param getFeedbackByStudentIdRequest body schemas.GetFeedbackByStudentIdRequest true "Get feedback by student ID request"
281
// @Success 200 {array} model.StudentFeedback
282
// @Router /feedback/student/{id} [put]
283
func (c *EnrollmentController) GetFeedbackByStudentId(ctx *gin.Context) {
1✔
284
        slog.Debug("Getting feedback by student ID", "studentId", ctx.Param("id"))
1✔
285
        studentID := ctx.Param("id")
1✔
286

1✔
287
        if studentID == "" {
1✔
288
                slog.Error("Invalid student ID")
×
289
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid student ID"})
×
290
                return
×
291
        }
×
292

293
        var getFeedbackByStudentIdRequest schemas.GetFeedbackByStudentIdRequest
1✔
294
        if err := ctx.ShouldBindJSON(&getFeedbackByStudentIdRequest); err != nil {
2✔
295
                slog.Error("Error binding get feedback by student ID request", "error", err)
1✔
296
                ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1✔
297
                return
1✔
298
        }
1✔
299

300
        feedback, err := c.enrollmentService.GetFeedbackByStudentId(studentID, getFeedbackByStudentIdRequest)
1✔
301
        if err != nil {
2✔
302
                slog.Error("Error getting feedback by student ID", "error", err)
1✔
303
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
304
                return
1✔
305
        }
1✔
306

307
        slog.Debug("Feedback retrieved", "studentId", studentID)
1✔
308
        ctx.JSON(http.StatusOK, feedback)
1✔
309
}
310

311
// @Summary Get student feedback summary
312
// @Description Get student feedback summary by student ID
313
// @Tags enrollments
314
// @Accept json
315
// @Produce json
316
// @Param id path string true "Student ID"
317
// @Success 200 {object} schemas.AiSummaryResponse
318
// @Router /feedback/student/{id}/summary [get]
319
func (c *EnrollmentController) GetStudentFeedbackSummary(ctx *gin.Context) {
×
320
        slog.Debug("Getting student feedback summary", "studentId", ctx.Param("id"))
×
321
        studentID := ctx.Param("id")
×
322

×
323
        if studentID == "" {
×
324
                slog.Error("Invalid student ID")
×
325
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid student ID"})
×
326
                return
×
327
        }
×
328

329
        feedbacks, err := c.enrollmentService.GetFeedbackByStudentId(studentID, schemas.GetFeedbackByStudentIdRequest{})
×
330
        if err != nil {
×
331
                slog.Error("Error getting student feedback", "error", err)
×
332
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
333
                return
×
334
        }
×
335

336
        if len(feedbacks) == 0 {
×
337
                slog.Error("No feedbacks found")
×
338
                ctx.JSON(http.StatusNotFound, gin.H{"error": "No feedbacks found"})
×
339
                return
×
340
        }
×
341

342
        summary, err := c.aiClient.SummarizeStudentFeedbacks(feedbacks)
×
343
        if err != nil {
×
344
                slog.Error("Error summarizing student feedback", "error", err)
×
345
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
346
                return
×
347
        }
×
348

349
        slog.Debug("Student feedback summary retrieved", "summary", summary)
×
NEW
350
        ctx.JSON(http.StatusOK, schemas.AiSummaryResponse{Summary: summary})
×
351
}
352

353
// @Summary Approve a student in a course
354
// @Description Approve a student by changing their enrollment status to completed
355
// @Tags enrollments
356
// @Accept json
357
// @Produce json
358
// @Param id path string true "Course ID"
359
// @Param studentId path string true "Student ID"
360
// @Success 200 {object} schemas.ApproveStudentResponse
361
// @Router /courses/{id}/students/{studentId}/approve [put]
362
func (c *EnrollmentController) ApproveStudent(ctx *gin.Context) {
1✔
363
        slog.Debug("Approving student", "courseId", ctx.Param("courseId"), "studentId", ctx.Param("studentId"))
1✔
364

1✔
365
        courseID := ctx.Param("id")
1✔
366
        studentID := ctx.Param("studentId")
1✔
367

1✔
368
        if courseID == "" {
2✔
369
                slog.Error("Invalid course ID")
1✔
370
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Course ID is required"})
1✔
371
                return
1✔
372
        }
1✔
373

374
        if studentID == "" {
2✔
375
                slog.Error("Invalid student ID")
1✔
376
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Student ID is required"})
1✔
377
                return
1✔
378
        }
1✔
379

380
        err := c.enrollmentService.ApproveStudent(studentID, courseID)
1✔
381
        if err != nil {
2✔
382
                slog.Error("Error approving student", "error", err, "studentId", studentID, "courseId", courseID)
1✔
383
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
384
                return
1✔
385
        }
1✔
386

387
        // Log activity if teacher is auxiliary
388
        teacherUUID := ctx.GetString("teacher_uuid")
1✔
389
        if teacherUUID != "" {
2✔
390
                c.activityService.LogActivityIfAuxTeacher(
1✔
391
                        courseID,
1✔
392
                        teacherUUID,
1✔
393
                        "APPROVE_STUDENT",
1✔
394
                        fmt.Sprintf("Approved student: %s", studentID),
1✔
395
                )
1✔
396
        }
1✔
397

398
        slog.Debug("Student approved successfully", "studentId", studentID, "courseId", courseID)
1✔
399
        ctx.JSON(http.StatusOK, schemas.ApproveStudentResponse{
1✔
400
                Message:   "Student approved successfully",
1✔
401
                StudentID: studentID,
1✔
402
                CourseID:  courseID,
1✔
403
        })
1✔
404
}
405

406
// @Summary Disapprove a student in a course
407
// @Description Disapprove a student by changing their enrollment status to dropped with a reason
408
// @Tags enrollments
409
// @Accept json
410
// @Produce json
411
// @Param id path string true "Course ID"
412
// @Param studentId path string true "Student ID"
413
// @Param disapproveRequest body schemas.DisapproveStudentRequest true "Disapprove request"
414
// @Success 200 {object} schemas.DisapproveStudentResponse
415
// @Router /courses/{id}/students/{studentId}/disapprove [put]
416
func (c *EnrollmentController) DisapproveStudent(ctx *gin.Context) {
1✔
417
        slog.Debug("Disapproving student", "courseId", ctx.Param("id"), "studentId", ctx.Param("studentId"))
1✔
418

1✔
419
        courseID := ctx.Param("id")
1✔
420
        studentID := ctx.Param("studentId")
1✔
421

1✔
422
        if courseID == "" {
2✔
423
                slog.Error("Invalid course ID")
1✔
424
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Course ID is required"})
1✔
425
                return
1✔
426
        }
1✔
427

428
        if studentID == "" {
2✔
429
                slog.Error("Invalid student ID")
1✔
430
                ctx.JSON(http.StatusBadRequest, gin.H{"error": "Student ID is required"})
1✔
431
                return
1✔
432
        }
1✔
433

434
        var disapproveRequest schemas.DisapproveStudentRequest
1✔
435
        if err := ctx.ShouldBindJSON(&disapproveRequest); err != nil {
2✔
436
                slog.Error("Error binding disapprove request", "error", err)
1✔
437
                ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1✔
438
                return
1✔
439
        }
1✔
440

441
        err := c.enrollmentService.DisapproveStudent(studentID, courseID, disapproveRequest.Reason)
1✔
442
        if err != nil {
2✔
443
                slog.Error("Error disapproving student", "error", err, "studentId", studentID, "courseId", courseID)
1✔
444
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1✔
445
                return
1✔
446
        }
1✔
447

448
        // Log activity if teacher is auxiliary
449
        teacherUUID := ctx.GetString("teacher_uuid")
1✔
450
        if teacherUUID != "" {
2✔
451
                c.activityService.LogActivityIfAuxTeacher(
1✔
452
                        courseID,
1✔
453
                        teacherUUID,
1✔
454
                        "DISAPPROVE_STUDENT",
1✔
455
                        fmt.Sprintf("Disapproved student: %s (reason: %s)", studentID, disapproveRequest.Reason),
1✔
456
                )
1✔
457
        }
1✔
458

459
        slog.Debug("Student disapproved successfully", "studentId", studentID, "courseId", courseID, "reason", disapproveRequest.Reason)
1✔
460
        ctx.JSON(http.StatusOK, schemas.DisapproveStudentResponse{
1✔
461
                Message:   "Student disapproved successfully",
1✔
462
                StudentID: studentID,
1✔
463
                CourseID:  courseID,
1✔
464
                Reason:    disapproveRequest.Reason,
1✔
465
        })
1✔
466
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc