feat: return premium catalog content on GET for client-side lock UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-06-11 01:14:04 -07:00
parent a4792206f7
commit 8409f69d56
13 changed files with 281 additions and 235 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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",
})
}

View File

@ -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,

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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: