package httpserver import ( "context" "Yimaru-Backend/internal/domain" lessonsvc "Yimaru-Backend/internal/services/lessons" coursessvc "Yimaru-Backend/internal/services/courses" modulesvc "Yimaru-Backend/internal/services/modules" programssvc "Yimaru-Backend/internal/services/programs" practicessvc "Yimaru-Backend/internal/services/practices" "errors" "fmt" "strings" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) 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 } if bypassSubscriptionForRole(role) { return c.Next() } if role != domain.RoleStudent && role != domain.RoleOpenLearner { return c.Next() } if domain.CategorySubscriptionGateDisabled { return c.Next() } tier, resolved, err := a.resolveLMSEffectiveAccessTier(c) if err != nil { switch { case errors.Is(err, programssvc.ErrProgramNotFound), errors.Is(err, coursessvc.ErrCourseNotFound), errors.Is(err, modulesvc.ErrModuleNotFound), errors.Is(err, lessonsvc.ErrLessonNotFound), errors.Is(err, practicessvc.ErrPracticeNotFound): return fiber.NewError(fiber.StatusNotFound, err.Error()) default: return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify content access") } } if !resolved || !tier.RequiresSubscription() { return c.Next() } active, err := a.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, domain.SubscriptionCategoryLearnEnglish) if err != nil { a.mongoLoggerSvc.Error("category subscription check failed", zap.Int64("userID", userID), zap.String("category", string(domain.SubscriptionCategoryLearnEnglish)), zap.String("path", c.Path()), zap.Error(err), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify subscription") } if !active { return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("An active %s subscription is required", humanizeSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish))) } return c.Next() } } func (a *App) resolveLMSEffectiveAccessTier(c *fiber.Ctx) (domain.ContentAccessTier, bool, error) { ctx := c.Context() routePath := c.Route().Path if strings.Contains(routePath, "/practices/:practiceId/questions") || strings.Contains(routePath, "/practices/:id") { practiceID, ok, err := parseRouteInt64(c, "practiceId") if err != nil { return "", false, err } if !ok { practiceID, ok, err = parseRouteInt64(c, "id") if err != nil { return "", false, err } if !ok { goto unresolved } } return a.lmsEffectiveTierForPractice(ctx, practiceID) } if lessonID, ok, err := parseRouteInt64(c, "lessonId"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForLesson(ctx, lessonID) } if moduleID, ok, err := parseRouteInt64(c, "moduleId"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForModule(ctx, moduleID) } if _, ok, err := parseRouteInt64(c, "courseId"); err != nil { return "", false, err } else if ok { return "", false, nil } switch { case strings.Contains(routePath, "/lessons/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForLesson(ctx, id) } case strings.Contains(routePath, "/modules/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForModule(ctx, id) } case strings.Contains(routePath, "/courses/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForCourse(ctx, id) } case strings.Contains(routePath, "/programs/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.lmsEffectiveTierForProgram(ctx, id) } } unresolved: return "", false, nil } func (a *App) lmsEffectiveTierForProgram(ctx context.Context, programID int64) (domain.ContentAccessTier, bool, error) { program, err := a.programSvc.GetByID(ctx, programID) if err != nil { return "", false, err } return program.AccessTier, true, nil } func (a *App) lmsEffectiveTierForCourse(ctx context.Context, courseID int64) (domain.ContentAccessTier, bool, error) { course, err := a.courseSvc.GetByID(ctx, courseID) if err != nil { return "", false, err } program, err := a.programSvc.GetByID(ctx, course.ProgramID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier), true, nil } func (a *App) lmsEffectiveTierForModule(ctx context.Context, moduleID int64) (domain.ContentAccessTier, bool, error) { module, err := a.moduleSvc.GetByID(ctx, moduleID) if err != nil { return "", false, err } course, err := a.courseSvc.GetByID(ctx, module.CourseID) if err != nil { return "", false, err } program, err := a.programSvc.GetByID(ctx, course.ProgramID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier), true, nil } func (a *App) lmsEffectiveTierForLesson(ctx context.Context, lessonID int64) (domain.ContentAccessTier, bool, error) { lesson, err := a.lessonSvc.GetByID(ctx, lessonID) if err != nil { return "", false, err } module, err := a.moduleSvc.GetByID(ctx, lesson.ModuleID) if err != nil { return "", false, err } course, err := a.courseSvc.GetByID(ctx, module.CourseID) if err != nil { return "", false, err } program, err := a.programSvc.GetByID(ctx, course.ProgramID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier, lesson.AccessTier), true, nil } func (a *App) resolveExamPrepEffectiveAccessTier(c *fiber.Ctx) (domain.ContentAccessTier, bool, error) { ctx := c.Context() routePath := c.Route().Path if _, ok, err := parseRouteInt64(c, "catalogCourseId"); err != nil { return "", false, err } else if ok { return "", false, nil } if _, ok, err := parseRouteInt64(c, "unitId"); err != nil { return "", false, err } else if ok { return "", false, nil } if _, ok, err := parseRouteInt64(c, "moduleId"); err != nil { return "", false, err } else if ok { return "", false, nil } if _, ok, err := parseRouteInt64(c, "lessonId"); err != nil { return "", false, err } else if ok { return "", false, nil } switch { case strings.Contains(routePath, "/catalog-courses/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.examPrepEffectiveTierForCatalogCourse(ctx, id) } case strings.Contains(routePath, "/units/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.examPrepEffectiveTierForUnit(ctx, id) } case strings.Contains(routePath, "/modules/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.examPrepEffectiveTierForModule(ctx, id) } case strings.Contains(routePath, "/lessons/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.examPrepEffectiveTierForLesson(ctx, id) } case strings.Contains(routePath, "/practices/:id"): if id, ok, err := parseRouteInt64(c, "id"); err != nil { return "", false, err } else if ok { return a.examPrepEffectiveTierForPractice(ctx, id) } } return "", false, nil } func (a *App) examPrepEffectiveTierForCatalogCourse(ctx context.Context, catalogCourseID int64) (domain.ContentAccessTier, bool, error) { cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, catalogCourseID) if err != nil { return "", false, err } return cc.AccessTier, true, nil } func (a *App) examPrepEffectiveTierForUnit(ctx context.Context, unitID int64) (domain.ContentAccessTier, bool, error) { unit, err := a.examPrepSvc.GetUnitByID(ctx, unitID) if err != nil { return "", false, err } cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier), true, nil } func (a *App) examPrepEffectiveTierForModule(ctx context.Context, moduleID int64) (domain.ContentAccessTier, bool, error) { module, err := a.examPrepSvc.GetModuleByID(ctx, moduleID) if err != nil { return "", false, err } unit, err := a.examPrepSvc.GetUnitByID(ctx, module.UnitID) if err != nil { return "", false, err } cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier, module.AccessTier), true, nil } func (a *App) examPrepEffectiveTierForLesson(ctx context.Context, lessonID int64) (domain.ContentAccessTier, bool, error) { lesson, err := a.examPrepSvc.GetLessonByID(ctx, lessonID) if err != nil { return "", false, err } module, err := a.examPrepSvc.GetModuleByID(ctx, lesson.UnitModuleID) if err != nil { return "", false, err } unit, err := a.examPrepSvc.GetUnitByID(ctx, module.UnitID) if err != nil { return "", false, err } cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID) if err != nil { return "", false, err } return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier), true, nil } func (a *App) examPrepEffectiveTierForPractice(ctx context.Context, practiceID int64) (domain.ContentAccessTier, bool, error) { practice, err := a.examPrepSvc.GetExamPrepPracticeByID(ctx, practiceID) if err != nil { return "", false, err } return a.examPrepEffectiveTierForLesson(ctx, practice.LessonID) } func (a *App) lmsEffectiveTierForPractice(ctx context.Context, practiceID int64) (domain.ContentAccessTier, bool, error) { practice, err := a.practiceSvc.GetByID(ctx, practiceID) if err != nil { return "", false, err } switch practice.ParentKind { case domain.ParentKindLesson: return a.lmsEffectiveTierForLesson(ctx, practice.ParentID) case domain.ParentKindModule: return a.lmsEffectiveTierForModule(ctx, practice.ParentID) case domain.ParentKindCourse: return a.lmsEffectiveTierForCourse(ctx, practice.ParentID) default: return "", false, nil } }