package handlers import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/services/lessons" "Yimaru-Backend/internal/services/modules" "context" "errors" "strconv" "github.com/gofiber/fiber/v2" ) // CreateLesson godoc // @Summary Create lesson // @Tags lessons // @Accept json // @Produce json // @Param moduleId path int true "Module ID" // @Param body body domain.CreateLessonInput true "Lesson" // @Router /api/v1/modules/{moduleId}/lessons [post] func (h *Handler) CreateLesson(c *fiber.Ctx) error { moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid module id", Error: err.Error(), }) } var req domain.CreateLessonInput if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: firstValidationError(valErrs), }) } les, err := h.lessonSvc.Create(c.Context(), moduleID, req) if err != nil { if errors.Is(err, modules.ErrModuleNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Module not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create lesson", Error: err.Error(), }) } actorID := c.Locals("user_id").(int64) actorRole := string(c.Locals("role").(domain.Role)) ip := c.IP() ua := c.Get("User-Agent") rid := les.ID go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonCreated, domain.ResourceLesson, &rid, "Created lesson: "+les.Title, nil, &ip, &ua) return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Lesson created successfully", Data: les, Success: true, StatusCode: fiber.StatusCreated, }) } // ListLessonsByModule godoc // @Tags lessons // @Param moduleId path int true "Module ID" // @Param limit query int false "Page size" default(20) // @Param offset query int false "Offset" default(0) // @Router /api/v1/modules/{moduleId}/lessons [get] func (h *Handler) ListLessonsByModule(c *fiber.Ctx) error { moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid module id", Error: err.Error(), }) } limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) publishedOnly := !h.canManageLessons(c) items, total, err := h.lessonSvc.ListByModule(c.Context(), moduleID, publishedOnly, int32(limit), int32(offset)) if err != nil { if errors.Is(err, modules.ErrModuleNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Module not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to list lessons", Error: err.Error(), }) } uid := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) for i := range items { if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &items[i]); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to build lesson list", Error: err.Error(), }) } } return c.JSON(domain.Response{ Message: "Lessons retrieved successfully", Data: fiber.Map{ "lessons": items, "total_count": total, "limit": limit, "offset": offset, }, Success: true, StatusCode: fiber.StatusOK, }) } // GetLesson godoc // @Tags lessons // @Param id path int true "Lesson ID" // @Router /api/v1/lessons/{id} [get] func (h *Handler) GetLesson(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson id", Error: err.Error(), }) } les, err := h.lessonSvc.GetByID(c.Context(), id) if err != nil { if errors.Is(err, lessons.ErrLessonNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to load lesson", Error: err.Error(), }) } if !les.VisibleToLearners() && !h.canManageLessons(c) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: lessons.ErrLessonNotFound.Error(), }) } uid := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &les); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to evaluate lesson access", Error: err.Error(), }) } if err := lmsBlockIfInaccessible(c, les.Access); err != nil { return err } return c.JSON(domain.Response{ Message: "Lesson retrieved successfully", Data: les, Success: true, StatusCode: fiber.StatusOK, }) } // UpdateLesson godoc // @Tags lessons // @Param id path int true "Lesson ID" // @Param body body domain.UpdateLessonInput true "Fields to update" // @Router /api/v1/lessons/{id} [put] func (h *Handler) UpdateLesson(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson id", Error: err.Error(), }) } var req domain.UpdateLessonInput if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } les, err := h.lessonSvc.Update(c.Context(), id, req) if err != nil { if errors.Is(err, lessons.ErrLessonNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update lesson", Error: err.Error(), }) } actorID := c.Locals("user_id").(int64) actorRole := string(c.Locals("role").(domain.Role)) ip := c.IP() ua := c.Get("User-Agent") rid := les.ID go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonUpdated, domain.ResourceLesson, &rid, "Updated lesson: "+les.Title, nil, &ip, &ua) return c.JSON(domain.Response{ Message: "Lesson updated successfully", Data: les, Success: true, StatusCode: fiber.StatusOK, }) } // DeleteLesson godoc // @Tags lessons // @Param id path int true "Lesson ID" // @Router /api/v1/lessons/{id} [delete] func (h *Handler) DeleteLesson(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson id", Error: err.Error(), }) } if err := h.lessonSvc.Delete(c.Context(), id); err != nil { if errors.Is(err, lessons.ErrLessonNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete lesson", Error: err.Error(), }) } actorID := c.Locals("user_id").(int64) actorRole := string(c.Locals("role").(domain.Role)) ip := c.IP() ua := c.Get("User-Agent") go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonDeleted, domain.ResourceLesson, &id, "Deleted lesson", nil, &ip, &ua) return c.JSON(domain.Response{ Message: "Lesson deleted successfully", Success: true, StatusCode: fiber.StatusOK, }) } // CompleteLesson godoc // @Summary Mark a lesson as completed // @Description Records lesson completion; may cascade to module, course, and program progress for the authenticated user. Learners must meet sequential prerequisites; staff bypass checks. // @Tags lessons // @Param id path int true "Lesson ID" // @Success 200 {object} domain.Response // @Failure 403 {object} domain.ErrorResponse // @Router /api/v1/lessons/{id}/complete [post] func (h *Handler) CompleteLesson(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson id", Error: err.Error(), }) } les, err := h.lessonSvc.GetByID(c.Context(), id) if err != nil { if errors.Is(err, lessons.ErrLessonNotFound) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to load lesson", Error: err.Error(), }) } role := c.Locals("role").(domain.Role) if role.IsCustomerLearnerRole() && !les.VisibleToLearners() { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Only published lessons can be completed", Error: "LESSON_NOT_PUBLISHED", }) } uid := c.Locals("user_id").(int64) if role.UsesLMSSequentialGating() { ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to verify lesson access", Error: err.Error(), }) } if !ok { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: reason, Error: "LMS_PREREQUISITE_NOT_MET", }) } } if err := h.lmsProgressSvc.CompleteLessonForUser(c.Context(), uid, id); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to record lesson progress", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Lesson marked complete", Success: true, StatusCode: fiber.StatusOK, }) }