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 {
|
func (a *App) RequireLMSSubscriptionUnlessFree() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
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)
|
role, userID, err := subscriptionScopedUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,33 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"Yimaru-Backend/internal/domain"
|
"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"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) learnerHasLearnEnglishSubscription(c *fiber.Ctx) (bool, error) {
|
func (h *Handler) learnerHasSubscriptionCategory(c *fiber.Ctx, category domain.SubscriptionCategory) (bool, error) {
|
||||||
return h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategoryLearnEnglish)
|
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)
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
if !role.IsCustomerLearnerRole() || domain.CategorySubscriptionGateDisabled {
|
if !role.IsCustomerLearnerRole() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !effectiveTier.RequiresSubscription() {
|
if !domain.IsLMSContentCategory(program.Category) {
|
||||||
return nil
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
}
|
Message: "Program not found",
|
||||||
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)),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -66,58 +65,191 @@ func applyExamPrepLessonEffectiveAccessTier(lesson *domain.ExamPrepLesson, catal
|
||||||
lesson.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier)
|
lesson.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func learnerCanViewEffectiveTier(hasSubscription bool, effectiveTier domain.ContentAccessTier) bool {
|
func annotateProgramsEffectiveAccessTier(items []domain.Program) {
|
||||||
return !effectiveTier.RequiresSubscription() || hasSubscription
|
for i := range items {
|
||||||
|
applyProgramEffectiveAccessTier(&items[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterProgramsForLearner(items []domain.Program, hasLearnEnglish bool) []domain.Program {
|
func annotateCoursesEffectiveAccessTier(items []domain.Course, program domain.Program) {
|
||||||
filtered := make([]domain.Program, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
applyProgramEffectiveAccessTier(&item)
|
|
||||||
if learnerCanViewEffectiveTier(hasLearnEnglish, item.EffectiveAccessTier) {
|
|
||||||
filtered = append(filtered, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterCoursesForLearner(items []domain.Course, program domain.Program, hasLearnEnglish bool) []domain.Course {
|
|
||||||
filtered := make([]domain.Course, 0, len(items))
|
|
||||||
for i := range items {
|
for i := range items {
|
||||||
applyCourseEffectiveAccessTier(&items[i], program)
|
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 {
|
func annotateExamPrepCatalogCoursesEffectiveAccessTier(items []domain.ExamPrepCatalogCourse) {
|
||||||
if domain.ExamPrepSubscriptionGateDisabled {
|
for i := range items {
|
||||||
out := make([]domain.ExamPrepCatalogCourse, 0, len(items))
|
applyExamPrepCatalogCourseEffectiveAccessTier(&items[i])
|
||||||
for _, item := range items {
|
}
|
||||||
applyExamPrepCatalogCourseEffectiveAccessTier(&item)
|
}
|
||||||
out = append(out, item)
|
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
}
|
|
||||||
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(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if role.IsCustomerLearnerRole() && publishedOnly {
|
if publishedOnly {
|
||||||
hasLearnEnglish, err := h.learnerHasLearnEnglishSubscription(c)
|
if program.ID == 0 {
|
||||||
|
p, err := h.programSvc.GetByID(c.Context(), programID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||||
Message: "Failed to verify Learn English subscription",
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Program not found",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
items = filterCoursesForLearner(items, program, hasLearnEnglish)
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
total = int64(len(items))
|
Message: "Failed to load program",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
program = p
|
||||||
|
}
|
||||||
|
annotateCoursesEffectiveAccessTier(items, program)
|
||||||
}
|
}
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
|
|
@ -219,9 +227,6 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
applyCourseEffectiveAccessTier(&course, p)
|
applyCourseEffectiveAccessTier(&course, p)
|
||||||
if err := h.ensureLearnerPremiumContentAccess(c, course.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil {
|
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(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := lmsBlockIfInaccessible(c, course.Access); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Course retrieved successfully",
|
Message: "Course retrieved successfully",
|
||||||
Data: course,
|
Data: course,
|
||||||
|
|
|
||||||
|
|
@ -61,64 +61,6 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
publishedOnly := !h.canManageExamPrepCatalogCourses(c)
|
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))
|
items, total, err := h.examPrepSvc.ListCatalogCourses(c.Context(), publishedOnly, int32(limit), int32(offset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -126,6 +68,7 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
annotateExamPrepCatalogCoursesEffectiveAccessTier(items)
|
||||||
if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, items); err != nil {
|
if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, items); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build catalog course list",
|
Message: "Failed to build catalog course list",
|
||||||
|
|
@ -221,9 +164,6 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
applyExamPrepCatalogCourseEffectiveAccessTier(&out)
|
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 {
|
if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build catalog course",
|
Message: "Failed to build catalog course",
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@ func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error {
|
||||||
Error: err.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 {
|
if err := h.applyExamPrepAccessLessons(c.Context(), c, items); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build lesson list",
|
Message: "Failed to build lesson list",
|
||||||
|
|
@ -198,6 +203,11 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
|
||||||
Error: examprep.ErrLessonNotFound.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 {
|
if err := h.applyExamPrepAccessLesson(c.Context(), c, &les); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build lesson",
|
Message: "Failed to build lesson",
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@ func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error {
|
||||||
Error: err.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 {
|
if err := h.applyExamPrepAccessModules(c.Context(), c, items); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build module list",
|
Message: "Failed to build module list",
|
||||||
|
|
@ -198,6 +203,11 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
|
||||||
Error: examprep.ErrModuleNotFound.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 {
|
if err := h.applyExamPrepAccessModule(c.Context(), c, &out); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build module",
|
Message: "Failed to build module",
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,11 @@ func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error {
|
||||||
Error: err.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 {
|
if err := h.applyExamPrepAccessUnits(c.Context(), c, items); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build unit list",
|
Message: "Failed to build unit list",
|
||||||
|
|
@ -206,6 +211,11 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
|
||||||
Error: examprep.ErrUnitNotFound.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 {
|
if err := h.applyExamPrepAccessUnit(c.Context(), c, &out); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to build unit",
|
Message: "Failed to build unit",
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@ func (h *Handler) ListLessonsByModule(c *fiber.Ctx) error {
|
||||||
Error: err.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)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
|
|
@ -152,6 +157,11 @@ func (h *Handler) GetLesson(c *fiber.Ctx) error {
|
||||||
Error: lessons.ErrLessonNotFound.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)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &les); err != nil {
|
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(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := lmsBlockIfInaccessible(c, les.Access); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Lesson retrieved successfully",
|
Message: "Lesson retrieved successfully",
|
||||||
Data: les,
|
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(),
|
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)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
|
|
@ -171,15 +176,17 @@ func (h *Handler) GetModule(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
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 {
|
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to evaluate module access",
|
Message: "Failed to evaluate module access",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := lmsBlockIfInaccessible(c, mod.Access); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Module retrieved successfully",
|
Message: "Module retrieved successfully",
|
||||||
Data: mod,
|
Data: mod,
|
||||||
|
|
|
||||||
|
|
@ -82,17 +82,7 @@ func (h *Handler) ListPrograms(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if role.IsCustomerLearnerRole() {
|
annotateProgramsEffectiveAccessTier(items)
|
||||||
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))
|
|
||||||
}
|
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &items[i]); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
applyProgramEffectiveAccessTier(&p)
|
applyProgramEffectiveAccessTier(&p)
|
||||||
if err := h.ensureLearnerPremiumContentAccess(c, p.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &p); err != nil {
|
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(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := lmsBlockIfInaccessible(c, p.Access); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Program retrieved successfully",
|
Message: "Program retrieved successfully",
|
||||||
Data: p,
|
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 {
|
func (a *App) RequireExamPrepSubscription() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
|
if isReadOnlyHTTPMethod(c.Method()) {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
role, userID, err := subscriptionScopedUser(c)
|
role, userID, err := subscriptionScopedUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -332,6 +335,15 @@ func subscriptionScopedUser(c *fiber.Ctx) (domain.Role, int64, error) {
|
||||||
return role, userID, nil
|
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 {
|
func bypassSubscriptionForRole(role domain.Role) bool {
|
||||||
switch role {
|
switch role {
|
||||||
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user