From 4055ad46f6221f39b2b09d42b083a516b18930fe Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 7 Apr 2026 06:13:03 -0700 Subject: [PATCH] add hierarchy endpoint and mobile integration guide Expose a dedicated human-language hierarchy endpoint aligned to category/subcategory/course/level/module/sub-module structure and add a complete learner mobile integration guide. Made-with: Cursor --- ...HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md | 292 ++++++++++++++++++ .../web_server/handlers/course_management.go | 146 +++++++++ internal/web_server/routes.go | 1 + 3 files changed, 439 insertions(+) create mode 100644 docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md diff --git a/docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md b/docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..9f7cde3 --- /dev/null +++ b/docs/HUMAN_LANGUAGE_MOBILE_INTEGRATION_GUIDE.md @@ -0,0 +1,292 @@ +# Human Language Mobile Integration Guide + +This guide explains how to integrate the new **Human Language** feature in the **Yimaru learner mobile app** (not admin). + +It is designed to keep the existing non-language hierarchy intact while introducing a dedicated CEFR-based flow for language learning. + +--- + +## 1) Scope and Goals + +### What is new + +- A dedicated backend API namespace for Human Language: + - `GET /api/v1/course-management/human-language/courses/:courseId/lessons?cefr_level=A1..C3` + - `POST /api/v1/course-management/human-language/lessons` (admin/instructor side) + - `PATCH /api/v1/course-management/human-language/lessons/:id` (admin/instructor side) +- CEFR levels are fixed to: + - `A1, A2, A3, B1, B2, B3, C1, C2, C3` +- No custom sub-levels in Human Language flow. + +### What remains unchanged + +- Existing non-human-language content hierarchy and APIs. +- Existing course/category/sub-course endpoints for non-language domains (programming, etc.). + +--- + +## 2) Target Hierarchy for Human Language + +- Course Category (Human Language) + - Course (e.g., English) + - CEFR Lesson Unit (A1..C3 only) + - Intro/lesson videos + - Practices + - Audio questions + +Backend implementation stores CEFR lesson units using the existing sub-course model, with CEFR mapped internally and validated strictly. + +--- + +## 3) Authentication and Access + +All Human Language endpoints are under `/api/v1` and require bearer token auth. + +- Header: + - `Authorization: Bearer ` + +### Permission notes + +- **Learner mobile app** typically needs only the `GET` endpoint. +- `POST` and `PATCH` are content-management endpoints and should generally be used by admin/instructor clients, not learner clients. + +If learner roles do not currently have `learning_tree.get`, coordinate RBAC assignment for read-only access to the `GET` endpoint. + +--- + +## 4) Endpoint Contracts + +## 4.1 Fetch lessons by CEFR level (learner-facing) + +### Request + +`GET /api/v1/course-management/human-language/courses/{courseId}/lessons?cefr_level={A1|A2|A3|B1|B2|B3|C1|C2|C3}` + +### Example + +`GET /api/v1/course-management/human-language/courses/12/lessons?cefr_level=B1` + +### Response (success) + +```json +{ + "message": "Human-language lessons retrieved successfully", + "data": { + "course_id": 12, + "course_title": "English", + "cefr_level": "B1", + "lessons": [ + { + "id": 201, + "course_id": 12, + "title": "B1 Module 1", + "description": "Intermediate conversational patterns", + "thumbnail": "https://.../thumb.jpg", + "display_order": 1, + "level": "B1", + "video_count": 3, + "practice_count": 2, + "videos": [ + { + "id": 9001, + "title": "B1 Intro", + "description": "Lesson intro", + "video_url": "https://...", + "duration": 420, + "resolution": "1080p", + "display_order": 1 + } + ], + "practices": [ + { + "id": 4401, + "title": "B1 Speaking Practice", + "status": "PUBLISHED", + "question_count": 10 + } + ] + } + ] + }, + "success": true, + "status_code": 200, + "metadata": null +} +``` + +### Validation errors + +- Invalid CEFR level: + - `400` with error message: `Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3` +- Course not in human language category: + - `400` with error message indicating invalid human-language course. + +--- + +## 4.2 Create Human Language lesson unit (admin/instructor) + +### Request + +`POST /api/v1/course-management/human-language/lessons` + +```json +{ + "course_id": 12, + "title": "A2 Module 1", + "description": "A2 speaking fundamentals", + "thumbnail": "https://...", + "display_order": 1, + "cefr_level": "A2" +} +``` + +### Response + +Returns created lesson metadata with CEFR-normalized level behavior. + +--- + +## 4.3 Update Human Language lesson unit (admin/instructor) + +### Request + +`PATCH /api/v1/course-management/human-language/lessons/{lessonId}` + +```json +{ + "title": "A2 Module 1 - Updated", + "description": "Updated description", + "cefr_level": "A3", + "is_active": true +} +``` + +--- + +## 5) Recommended Mobile App Integration Flow + +## Step 1: Discover courses under Human Language category + +Use existing endpoints: + +1. `GET /course-management/categories` +2. Pick category where name indicates human language. +3. `GET /course-management/categories/:categoryId/courses` + +## Step 2: Select CEFR level in UI + +Use static list in app: + +- `A1, A2, A3, B1, B2, B3, C1, C2, C3` + +Do not allow custom levels in mobile UI. + +## Step 3: Fetch lessons by selected CEFR level + +Call: + +- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=` + +Render grouped lessons with nested videos/practices from response. + +## Step 4: Navigate to learning/practice screens + +- Lesson video playback uses returned `videos[]`. +- Practice entry uses returned `practices[]` IDs and existing practice-question endpoints. + +--- + +## 6) Mobile UI/UX Recommendations + +- Use CEFR tabs/segmented control (`A1`...`C3`) at top. +- Cache last selected level per course. +- Show empty state per level: + - "No lessons available for this level yet." +- Sort by `display_order` and maintain backend order. +- Show badges: + - video count + - practice count + +--- + +## 7) Error Handling and Resilience + +- `400` invalid level: reset selection to previous valid CEFR value and show toast/snackbar. +- `401/403`: trigger token refresh / re-login flow. +- `5xx`: show retry UI with exponential backoff. +- If category/course fetch succeeds but level fetch fails, keep course visible and allow manual retry. + +--- + +## 8) Data Model Mapping (Mobile DTO) + +Suggested DTO for learner app: + +```ts +type CefrLevel = "A1"|"A2"|"A3"|"B1"|"B2"|"B3"|"C1"|"C2"|"C3"; + +interface HumanLanguageLessonDTO { + id: number; + courseId: number; + title: string; + description?: string; + thumbnail?: string; + order: number; + level: CefrLevel; + videoCount: number; + practiceCount: number; + videos: { + id: number; + title: string; + url: string; + duration: number; + order: number; + }[]; + practices: { + id: number; + title: string; + status: string; + questionCount: number; + }[]; +} +``` + +--- + +## 9) Backward Compatibility + +- Existing non-language content (programming and other categories) continues to use current APIs and hierarchy unchanged. +- New Human Language endpoint is additive and isolated. +- Mobile app can progressively enable the new flow for language categories only. + +--- + +## 10) QA Checklist for Mobile Team + +- [ ] Category discovery correctly identifies Human Language category. +- [ ] CEFR selector only allows A1..C3. +- [ ] Fetch by CEFR level returns only matching lessons. +- [ ] Video/practice counts match rendered lists. +- [ ] Empty-level state works. +- [ ] Unauthorized/session-expired flow works. +- [ ] Non-language courses still load via existing app flow. + +--- + +## 11) Example Call Matrix + +- Load language categories/courses: + - `GET /course-management/categories` + - `GET /course-management/categories/:id/courses` +- Load A1 lessons: + - `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=A1` +- Load B2 lessons: + - `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=B2` + +--- + +For backend ownership questions, refer to: + +- `internal/web_server/handlers/course_management.go` +- `internal/web_server/routes.go` + diff --git a/internal/web_server/handlers/course_management.go b/internal/web_server/handlers/course_management.go index 33928ab..4db0227 100644 --- a/internal/web_server/handlers/course_management.go +++ b/internal/web_server/handlers/course_management.go @@ -697,6 +697,42 @@ type getHumanLanguageLessonsRes struct { Lessons []humanLanguageLessonRes `json:"lessons"` } +type humanLanguageSubModuleRes struct { + ID int64 `json:"id"` + Title string `json:"title"` + Videos []domain.LearningPathVideo `json:"videos"` + Practices []domain.LearningPathPractice `json:"practices"` +} + +type humanLanguageModuleRes struct { + ID int64 `json:"id"` + Title string `json:"title"` + SubModules []humanLanguageSubModuleRes `json:"sub_modules"` +} + +type humanLanguageLevelRes struct { + Level string `json:"level"` + Modules []humanLanguageModuleRes `json:"modules"` +} + +type humanLanguageCourseTreeRes struct { + CourseID int64 `json:"course_id"` + CourseName string `json:"course_name"` + Levels []humanLanguageLevelRes `json:"levels"` +} + +type humanLanguageSubCategoryTreeRes struct { + SubCategoryID int64 `json:"sub_category_id"` + SubCategoryName string `json:"sub_category_name"` + Courses []humanLanguageCourseTreeRes `json:"courses"` +} + +type humanLanguageHierarchyRes struct { + CategoryID int64 `json:"category_id"` + CategoryName string `json:"category_name"` + SubCategories []humanLanguageSubCategoryTreeRes `json:"sub_categories"` +} + // CreateHumanLanguageLesson godoc // @Summary Create human-language lesson unit // @Description Creates a lesson unit under a human-language course using CEFR level (A1..C3) @@ -860,6 +896,116 @@ func (h *Handler) GetHumanLanguageLessonsByCourse(c *fiber.Ctx) error { }) } +// GetHumanLanguageHierarchy godoc +// @Summary Get full human-language hierarchy +// @Description Returns Category -> SubCategory -> Course -> Level -> Module -> SubModule with videos/practices +// @Tags human-language +// @Produce json +// @Success 200 {object} domain.Response +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/human-language/hierarchy [get] +func (h *Handler) GetHumanLanguageHierarchy(c *fiber.Ctx) error { + categories, _, err := h.courseMgmtSvc.GetAllCourseCategories(c.Context(), 1000, 0) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to load course categories", + Error: err.Error(), + }) + } + + var humanCategory *domain.CourseCategory + for _, cat := range categories { + name := strings.ToLower(strings.TrimSpace(cat.Name)) + if strings.Contains(name, "human language") || strings.Contains(name, "language") { + cc := cat + humanCategory = &cc + break + } + } + + if humanCategory == nil { + return c.JSON(domain.Response{ + Message: "Human-language hierarchy retrieved successfully", + Data: humanLanguageHierarchyRes{ + CategoryID: 0, + CategoryName: "Human Language", + SubCategories: []humanLanguageSubCategoryTreeRes{}, + }, + }) + } + + courses, _, err := h.courseMgmtSvc.GetCoursesByCategory(c.Context(), humanCategory.ID, 1000, 0) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to load human-language subcategories", + Error: err.Error(), + }) + } + + subCategories := make([]humanLanguageSubCategoryTreeRes, 0, len(courses)) + for _, course := range courses { + path, pathErr := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), course.ID) + if pathErr != nil { + continue + } + + levelsMap := map[string][]humanLanguageModuleRes{} + for _, sc := range path.SubCourses { + levelKey := strings.ToUpper(strings.TrimSpace(sc.SubLevel)) + if !isValidHumanLanguageCEFRLevel(levelKey) { + continue + } + + module := humanLanguageModuleRes{ + ID: sc.ID, + Title: sc.Title, + SubModules: []humanLanguageSubModuleRes{ + { + ID: sc.ID, + Title: sc.Title, + Videos: sc.Videos, + Practices: sc.Practices, + }, + }, + } + levelsMap[levelKey] = append(levelsMap[levelKey], module) + } + + levels := make([]humanLanguageLevelRes, 0, 9) + for _, cefr := range []string{ + string(domain.SubCourseSubLevelA1), string(domain.SubCourseSubLevelA2), string(domain.SubCourseSubLevelA3), + string(domain.SubCourseSubLevelB1), string(domain.SubCourseSubLevelB2), string(domain.SubCourseSubLevelB3), + string(domain.SubCourseSubLevelC1), string(domain.SubCourseSubLevelC2), string(domain.SubCourseSubLevelC3), + } { + levels = append(levels, humanLanguageLevelRes{ + Level: cefr, + Modules: levelsMap[cefr], + }) + } + + subCategories = append(subCategories, humanLanguageSubCategoryTreeRes{ + SubCategoryID: course.ID, + SubCategoryName: course.Title, + Courses: []humanLanguageCourseTreeRes{ + { + CourseID: course.ID, + CourseName: course.Title, + Levels: levels, + }, + }, + }) + } + + return c.JSON(domain.Response{ + Message: "Human-language hierarchy retrieved successfully", + Data: humanLanguageHierarchyRes{ + CategoryID: humanCategory.ID, + CategoryName: humanCategory.Name, + SubCategories: subCategories, + }, + }) +} + // CreateSubCourse godoc // @Summary Create a new sub-course // @Description Creates a new sub-course under a specific course diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 929d42d..56d425c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -128,6 +128,7 @@ func (a *App) initAppRoutes() { // Learning Tree groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree) groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath) + groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageHierarchy) groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse) groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson) groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)