feat: return premium catalog content on GET for client-side lock UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a4792206f7
commit
8409f69d56
|
|
@ -19,6 +19,11 @@ import (
|
|||
|
||||
func (a *App) RequireLMSSubscriptionUnlessFree() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Reads return catalog metadata (including effective_access_tier) for client-side lock UI.
|
||||
// Subscription is enforced on consumption routes (complete, questions, video heartbeat).
|
||||
if isReadOnlyHTTPMethod(c.Method()) {
|
||||
return c.Next()
|
||||
}
|
||||
role, userID, err := subscriptionScopedUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -1,34 +1,33 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"fmt"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/examprep"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"Yimaru-Backend/internal/services/programs"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handler) learnerHasLearnEnglishSubscription(c *fiber.Ctx) (bool, error) {
|
||||
return h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategoryLearnEnglish)
|
||||
func (h *Handler) learnerHasSubscriptionCategory(c *fiber.Ctx, category domain.SubscriptionCategory) (bool, error) {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
return false, fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
return h.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, category)
|
||||
}
|
||||
|
||||
func (h *Handler) ensureLearnerPremiumContentAccess(c *fiber.Ctx, effectiveTier domain.ContentAccessTier, category domain.SubscriptionCategory) error {
|
||||
func (h *Handler) blockLearnerIfNotLMSProgram(c *fiber.Ctx, program domain.Program) error {
|
||||
role, _ := c.Locals("role").(domain.Role)
|
||||
if !role.IsCustomerLearnerRole() || domain.CategorySubscriptionGateDisabled {
|
||||
if !role.IsCustomerLearnerRole() {
|
||||
return nil
|
||||
}
|
||||
if !effectiveTier.RequiresSubscription() {
|
||||
return nil
|
||||
}
|
||||
active, err := h.learnerHasSubscriptionCategory(c, category)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to verify %s subscription", category),
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if !active {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: fmt.Sprintf("An active %s subscription is required", humanizeSubscriptionCategory(category)),
|
||||
if !domain.IsLMSContentCategory(program.Category) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Program not found",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
|
|
@ -66,58 +65,191 @@ func applyExamPrepLessonEffectiveAccessTier(lesson *domain.ExamPrepLesson, catal
|
|||
lesson.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier)
|
||||
}
|
||||
|
||||
func learnerCanViewEffectiveTier(hasSubscription bool, effectiveTier domain.ContentAccessTier) bool {
|
||||
return !effectiveTier.RequiresSubscription() || hasSubscription
|
||||
}
|
||||
|
||||
func filterProgramsForLearner(items []domain.Program, hasLearnEnglish bool) []domain.Program {
|
||||
filtered := make([]domain.Program, 0, len(items))
|
||||
for _, item := range items {
|
||||
applyProgramEffectiveAccessTier(&item)
|
||||
if learnerCanViewEffectiveTier(hasLearnEnglish, item.EffectiveAccessTier) {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
func annotateProgramsEffectiveAccessTier(items []domain.Program) {
|
||||
for i := range items {
|
||||
applyProgramEffectiveAccessTier(&items[i])
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterCoursesForLearner(items []domain.Course, program domain.Program, hasLearnEnglish bool) []domain.Course {
|
||||
filtered := make([]domain.Course, 0, len(items))
|
||||
func annotateCoursesEffectiveAccessTier(items []domain.Course, program domain.Program) {
|
||||
for i := range items {
|
||||
applyCourseEffectiveAccessTier(&items[i], program)
|
||||
if learnerCanViewEffectiveTier(hasLearnEnglish, items[i].EffectiveAccessTier) {
|
||||
filtered = append(filtered, items[i])
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterExamPrepCatalogCoursesForLearner(items []domain.ExamPrepCatalogCourse, hasIELTS, hasDuolingo bool) []domain.ExamPrepCatalogCourse {
|
||||
if domain.ExamPrepSubscriptionGateDisabled {
|
||||
out := make([]domain.ExamPrepCatalogCourse, 0, len(items))
|
||||
for _, item := range items {
|
||||
applyExamPrepCatalogCourseEffectiveAccessTier(&item)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
func annotateExamPrepCatalogCoursesEffectiveAccessTier(items []domain.ExamPrepCatalogCourse) {
|
||||
for i := range items {
|
||||
applyExamPrepCatalogCourseEffectiveAccessTier(&items[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) annotateLMSModulesEffectiveAccessTier(ctx context.Context, courseID int64, items []domain.Module) error {
|
||||
course, err := h.courseSvc.GetByID(ctx, courseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
program, err := h.programSvc.GetByID(ctx, course.ProgramID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
applyModuleEffectiveAccessTier(&items[i], program, course)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateLMSModuleEffectiveAccessTier(ctx context.Context, module *domain.Module) error {
|
||||
course, err := h.courseSvc.GetByID(ctx, module.CourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
program, err := h.programSvc.GetByID(ctx, course.ProgramID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyModuleEffectiveAccessTier(module, program, course)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateLMSLessonsEffectiveAccessTier(ctx context.Context, moduleID int64, items []domain.Lesson) error {
|
||||
module, err := h.moduleSvc.GetByID(ctx, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
course, err := h.courseSvc.GetByID(ctx, module.CourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
program, err := h.programSvc.GetByID(ctx, course.ProgramID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
applyLessonEffectiveAccessTier(&items[i], program, course, module)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateLMSLessonEffectiveAccessTier(ctx context.Context, lesson *domain.Lesson) error {
|
||||
module, err := h.moduleSvc.GetByID(ctx, lesson.ModuleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
course, err := h.courseSvc.GetByID(ctx, module.CourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
program, err := h.programSvc.GetByID(ctx, course.ProgramID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyLessonEffectiveAccessTier(lesson, program, course, module)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepUnitsEffectiveAccessTier(ctx context.Context, catalogCourseID int64, items []domain.ExamPrepUnit) error {
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, catalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
applyExamPrepUnitEffectiveAccessTier(&items[i], cc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepUnitEffectiveAccessTier(ctx context.Context, unit *domain.ExamPrepUnit) error {
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyExamPrepUnitEffectiveAccessTier(unit, cc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepModulesEffectiveAccessTier(ctx context.Context, unitID int64, items []domain.ExamPrepModule) error {
|
||||
unit, err := h.examPrepSvc.GetUnitByID(ctx, unitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
applyExamPrepModuleEffectiveAccessTier(&items[i], cc, unit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepModuleEffectiveAccessTier(ctx context.Context, module *domain.ExamPrepModule) error {
|
||||
unit, err := h.examPrepSvc.GetUnitByID(ctx, module.UnitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyExamPrepModuleEffectiveAccessTier(module, cc, unit)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepLessonsEffectiveAccessTier(ctx context.Context, moduleID int64, items []domain.ExamPrepLesson) error {
|
||||
module, err := h.examPrepSvc.GetModuleByID(ctx, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unit, err := h.examPrepSvc.GetUnitByID(ctx, module.UnitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
applyExamPrepLessonEffectiveAccessTier(&items[i], cc, unit, module)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) annotateExamPrepLessonEffectiveAccessTier(ctx context.Context, lesson *domain.ExamPrepLesson) error {
|
||||
module, err := h.examPrepSvc.GetModuleByID(ctx, lesson.UnitModuleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unit, err := h.examPrepSvc.GetUnitByID(ctx, module.UnitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := h.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyExamPrepLessonEffectiveAccessTier(lesson, cc, unit, module)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapLMSEffectiveAccessTierError(err error) (int, string) {
|
||||
switch {
|
||||
case err == nil:
|
||||
return 0, ""
|
||||
case err == programs.ErrProgramNotFound, err == courses.ErrCourseNotFound, err == modules.ErrModuleNotFound:
|
||||
return 404, "Not found"
|
||||
default:
|
||||
return 500, "Failed to build content access metadata"
|
||||
}
|
||||
}
|
||||
|
||||
func mapExamPrepEffectiveAccessTierError(err error) (int, string) {
|
||||
switch {
|
||||
case err == nil:
|
||||
return 0, ""
|
||||
case err == examprep.ErrCatalogCourseNotFound, err == examprep.ErrUnitNotFound,
|
||||
err == examprep.ErrModuleNotFound, err == examprep.ErrLessonNotFound:
|
||||
return 404, "Not found"
|
||||
default:
|
||||
return 500, "Failed to build content access metadata"
|
||||
}
|
||||
filtered := make([]domain.ExamPrepCatalogCourse, 0, len(items))
|
||||
for _, item := range items {
|
||||
applyExamPrepCatalogCourseEffectiveAccessTier(&item)
|
||||
if !item.EffectiveAccessTier.RequiresSubscription() {
|
||||
filtered = append(filtered, item)
|
||||
continue
|
||||
}
|
||||
switch domain.SubscriptionCategory(item.Category) {
|
||||
case domain.SubscriptionCategoryIELTS:
|
||||
if hasIELTS {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
case domain.SubscriptionCategoryDuolingo:
|
||||
if hasDuolingo {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,16 +134,24 @@ func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if role.IsCustomerLearnerRole() && publishedOnly {
|
||||
hasLearnEnglish, err := h.learnerHasLearnEnglishSubscription(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to verify Learn English subscription",
|
||||
Error: err.Error(),
|
||||
})
|
||||
if publishedOnly {
|
||||
if program.ID == 0 {
|
||||
p, err := h.programSvc.GetByID(c.Context(), programID)
|
||||
if err != nil {
|
||||
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Program not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load program",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
program = p
|
||||
}
|
||||
items = filterCoursesForLearner(items, program, hasLearnEnglish)
|
||||
total = int64(len(items))
|
||||
annotateCoursesEffectiveAccessTier(items, program)
|
||||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
for i := range items {
|
||||
|
|
@ -219,9 +227,6 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
|
|||
return err
|
||||
}
|
||||
applyCourseEffectiveAccessTier(&course, p)
|
||||
if err := h.ensureLearnerPremiumContentAccess(c, course.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
|
||||
return err
|
||||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil {
|
||||
|
|
@ -230,9 +235,6 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := lmsBlockIfInaccessible(c, course.Access); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course retrieved successfully",
|
||||
Data: course,
|
||||
|
|
|
|||
|
|
@ -61,64 +61,6 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
|
|||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
publishedOnly := !h.canManageExamPrepCatalogCourses(c)
|
||||
|
||||
role, _ := c.Locals("role").(domain.Role)
|
||||
if role.IsCustomerLearnerRole() {
|
||||
hasIELTS, err := h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategoryIELTS)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to verify IELTS subscription",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
hasDuolingo, err := h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategoryDuolingo)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to verify Duolingo subscription",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
allItems, _, err := h.examPrepSvc.ListCatalogCourses(c.Context(), publishedOnly, 200, 0)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to list catalog courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
filtered := filterExamPrepCatalogCoursesForLearner(allItems, hasIELTS, hasDuolingo)
|
||||
|
||||
total := len(filtered)
|
||||
start := offset
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
page := filtered[start:end]
|
||||
if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, page); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build catalog course list",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Catalog courses retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"catalog_courses": page,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
items, total, err := h.examPrepSvc.ListCatalogCourses(c.Context(), publishedOnly, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
|
|
@ -126,6 +68,7 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
annotateExamPrepCatalogCoursesEffectiveAccessTier(items)
|
||||
if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, items); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build catalog course list",
|
||||
|
|
@ -221,9 +164,6 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
applyExamPrepCatalogCourseEffectiveAccessTier(&out)
|
||||
if err := h.ensureLearnerExamPrepContentAccess(c, out.Category, out.EffectiveAccessTier); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build catalog course",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepLessonsEffectiveAccessTier(c.Context(), moduleID, items); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessLessons(c.Context(), c, items); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build lesson list",
|
||||
|
|
@ -198,6 +203,11 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
|
|||
Error: examprep.ErrLessonNotFound.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepLessonEffectiveAccessTier(c.Context(), &les); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessLesson(c.Context(), c, &les); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build lesson",
|
||||
|
|
|
|||
|
|
@ -96,6 +96,11 @@ func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepModulesEffectiveAccessTier(c.Context(), unitID, items); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessModules(c.Context(), c, items); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build module list",
|
||||
|
|
@ -198,6 +203,11 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
|
|||
Error: examprep.ErrModuleNotFound.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepModuleEffectiveAccessTier(c.Context(), &out); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessModule(c.Context(), c, &out); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build module",
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepUnitsEffectiveAccessTier(c.Context(), catalogCourseID, items); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessUnits(c.Context(), c, items); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build unit list",
|
||||
|
|
@ -206,6 +211,11 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
|
|||
Error: examprep.ErrUnitNotFound.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateExamPrepUnitEffectiveAccessTier(c.Context(), &out); err != nil {
|
||||
if status, msg := mapExamPrepEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.applyExamPrepAccessUnit(c.Context(), c, &out); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build unit",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ func (h *Handler) ListLessonsByModule(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateLMSLessonsEffectiveAccessTier(c.Context(), moduleID, items); err != nil {
|
||||
if status, msg := mapLMSEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
for i := range items {
|
||||
|
|
@ -152,6 +157,11 @@ func (h *Handler) GetLesson(c *fiber.Ctx) error {
|
|||
Error: lessons.ErrLessonNotFound.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateLMSLessonEffectiveAccessTier(c.Context(), &les); err != nil {
|
||||
if status, msg := mapLMSEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.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 {
|
||||
|
|
@ -160,9 +170,6 @@ func (h *Handler) GetLesson(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := lmsBlockIfInaccessible(c, les.Access); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Lesson retrieved successfully",
|
||||
Data: les,
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// lmsBlockIfInaccessible returns a 403 response when a learner is blocked; otherwise nil.
|
||||
func lmsBlockIfInaccessible(c *fiber.Ctx, a *domain.LMSEntityAccess) error {
|
||||
if a == nil || a.IsAccessible {
|
||||
return nil
|
||||
}
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: a.Reason,
|
||||
Error: "LMS_PREREQUISITE_NOT_MET",
|
||||
})
|
||||
}
|
||||
|
|
@ -114,6 +114,11 @@ func (h *Handler) ListModulesByCourse(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.annotateLMSModulesEffectiveAccessTier(c.Context(), courseID, items); err != nil {
|
||||
if status, msg := mapLMSEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
for i := range items {
|
||||
|
|
@ -171,15 +176,17 @@ func (h *Handler) GetModule(c *fiber.Ctx) error {
|
|||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
if err := h.annotateLMSModuleEffectiveAccessTier(c.Context(), &mod); err != nil {
|
||||
if status, msg := mapLMSEffectiveAccessTierError(err); status != 0 {
|
||||
return c.Status(status).JSON(domain.ErrorResponse{Message: msg, Error: err.Error()})
|
||||
}
|
||||
}
|
||||
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to evaluate module access",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := lmsBlockIfInaccessible(c, mod.Access); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Module retrieved successfully",
|
||||
Data: mod,
|
||||
|
|
|
|||
|
|
@ -82,17 +82,7 @@ func (h *Handler) ListPrograms(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if role.IsCustomerLearnerRole() {
|
||||
hasLearnEnglish, err := h.learnerHasLearnEnglishSubscription(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to verify Learn English subscription",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
items = filterProgramsForLearner(items, hasLearnEnglish)
|
||||
total = int64(len(items))
|
||||
}
|
||||
annotateProgramsEffectiveAccessTier(items)
|
||||
uid := c.Locals("user_id").(int64)
|
||||
for i := range items {
|
||||
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &items[i]); err != nil {
|
||||
|
|
@ -154,9 +144,6 @@ func (h *Handler) GetProgram(c *fiber.Ctx) error {
|
|||
return err
|
||||
}
|
||||
applyProgramEffectiveAccessTier(&p)
|
||||
if err := h.ensureLearnerPremiumContentAccess(c, p.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
|
||||
return err
|
||||
}
|
||||
uid := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &p); err != nil {
|
||||
|
|
@ -165,9 +152,6 @@ func (h *Handler) GetProgram(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := lmsBlockIfInaccessible(c, p.Access); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Program retrieved successfully",
|
||||
Data: p,
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handler) learnerHasSubscriptionCategory(c *fiber.Ctx, category domain.SubscriptionCategory) (bool, error) {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
return false, fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
return h.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, category)
|
||||
}
|
||||
|
||||
func (h *Handler) ensureLearnerExamPrepContentAccess(c *fiber.Ctx, contentCategory string, effectiveTier domain.ContentAccessTier) error {
|
||||
role, _ := c.Locals("role").(domain.Role)
|
||||
if !role.IsCustomerLearnerRole() || domain.ExamPrepSubscriptionGateDisabled {
|
||||
return nil
|
||||
}
|
||||
if !domain.IsExamPrepContentCategory(contentCategory) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Catalog course not found",
|
||||
})
|
||||
}
|
||||
return h.ensureLearnerPremiumContentAccess(c, effectiveTier, domain.SubscriptionCategory(contentCategory))
|
||||
}
|
||||
|
||||
func (h *Handler) blockLearnerIfNotLMSProgram(c *fiber.Ctx, program domain.Program) error {
|
||||
role, _ := c.Locals("role").(domain.Role)
|
||||
if !role.IsCustomerLearnerRole() {
|
||||
return nil
|
||||
}
|
||||
if !domain.IsLMSContentCategory(program.Category) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Program not found",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func humanizeSubscriptionCategory(category domain.SubscriptionCategory) string {
|
||||
switch category {
|
||||
case domain.SubscriptionCategoryLearnEnglish:
|
||||
return "learn english"
|
||||
case domain.SubscriptionCategoryIELTS:
|
||||
return "IELTS"
|
||||
case domain.SubscriptionCategoryDuolingo:
|
||||
return "Duolingo"
|
||||
default:
|
||||
return string(category)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +247,9 @@ func (a *App) RequireSubscriptionCategory(category domain.SubscriptionCategory)
|
|||
|
||||
func (a *App) RequireExamPrepSubscription() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if isReadOnlyHTTPMethod(c.Method()) {
|
||||
return c.Next()
|
||||
}
|
||||
role, userID, err := subscriptionScopedUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -332,6 +335,15 @@ func subscriptionScopedUser(c *fiber.Ctx) (domain.Role, int64, error) {
|
|||
return role, userID, nil
|
||||
}
|
||||
|
||||
func isReadOnlyHTTPMethod(method string) bool {
|
||||
switch method {
|
||||
case fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func bypassSubscriptionForRole(role domain.Role) bool {
|
||||
switch role {
|
||||
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user