From bc2357374bf7b121cf273f2f04f811e382b0ac6a Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 8 May 2026 11:57:11 -0700 Subject: [PATCH] Add practice-existence flags and refresh API contracts. Expose has_practice booleans for LMS and pre-exam hierarchy entities, wire SQL/repository mappings, and regenerate SQLC/Swagger artifacts. Also update the Resend sender display name for outbound emails. Co-authored-by: Cursor --- db/query/exam_prep_catalog_courses.sql | 23 ++++- db/query/exam_prep_unit_module_lessons.sql | 17 +++- db/query/exam_prep_unit_modules.sql | 14 ++- db/query/exam_prep_units.sql | 15 +++- db/query/lms_courses.sql | 22 ++++- db/query/lms_lessons.sql | 18 +++- db/query/lms_modules.sql | 20 ++++- docs/docs.go | 87 +++++++++++++++++++ docs/swagger.json | 87 +++++++++++++++++++ docs/swagger.yaml | 58 +++++++++++++ gen/db/exam_prep_catalog_courses.sql.go | 41 +++++++-- gen/db/exam_prep_unit_module_lessons.sql.go | 37 ++++++-- gen/db/exam_prep_unit_modules.sql.go | 34 ++++++-- gen/db/exam_prep_units.sql.go | 34 ++++++-- gen/db/lms_courses.sql.go | 41 +++++++-- gen/db/lms_lessons.sql.go | 38 ++++++-- gen/db/lms_modules.sql.go | 40 +++++++-- internal/domain/course.go | 1 + internal/domain/exam_prep_catalog_course.go | 1 + internal/domain/exam_prep_lesson.go | 1 + internal/domain/exam_prep_module.go | 1 + internal/domain/exam_prep_unit.go | 1 + internal/domain/lesson.go | 1 + internal/domain/module.go | 1 + .../repository/exam_prep_catalog_courses.go | 13 ++- .../exam_prep_unit_module_lessons.go | 20 ++++- internal/repository/exam_prep_unit_modules.go | 15 +++- internal/repository/exam_prep_units.go | 14 ++- internal/repository/lms_courses.go | 14 ++- internal/repository/lms_lessons.go | 20 ++++- internal/repository/lms_modules.go | 20 ++++- internal/services/messenger/email.go | 2 +- .../handlers/question_type_builder.go | 64 ++++++++++++++ internal/web_server/handlers/questions.go | 53 +++++++++++ internal/web_server/routes.go | 26 +++--- 35 files changed, 811 insertions(+), 83 deletions(-) diff --git a/db/query/exam_prep_catalog_courses.sql b/db/query/exam_prep_catalog_courses.sql index 193a0be..f2789f0 100644 --- a/db/query/exam_prep_catalog_courses.sql +++ b/db/query/exam_prep_catalog_courses.sql @@ -12,9 +12,18 @@ RETURNING *; -- name: ExamPrepGetCatalogCourseByID :one -SELECT * -FROM exam_prep.catalog_courses -WHERE id = $1; +SELECT + c.*, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + WHERE u.catalog_course_id = c.id + ) AS has_practice +FROM exam_prep.catalog_courses c +WHERE c.id = $1; -- name: ExamPrepListCatalogCourses :many WITH catalog_course_counts AS ( @@ -38,6 +47,14 @@ SELECT COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + WHERE u.catalog_course_id = c.id + ) AS has_practice, c.created_at, c.updated_at FROM exam_prep.catalog_courses c diff --git a/db/query/exam_prep_unit_module_lessons.sql b/db/query/exam_prep_unit_module_lessons.sql index 45d6491..2e38eec 100644 --- a/db/query/exam_prep_unit_module_lessons.sql +++ b/db/query/exam_prep_unit_module_lessons.sql @@ -16,9 +16,15 @@ RETURNING *; -- name: ExamPrepGetUnitModuleLessonByID :one -SELECT * -FROM exam_prep.unit_module_lessons -WHERE id = $1; +SELECT + l.*, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + WHERE p.unit_module_lesson_id = l.id + ) AS has_practice +FROM exam_prep.unit_module_lessons l +WHERE l.id = $1; -- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many SELECT @@ -39,6 +45,11 @@ SELECT l.thumbnail, l.description, l.sort_order, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + WHERE p.unit_module_lesson_id = l.id + ) AS has_practice, l.created_at, l.updated_at FROM exam_prep.unit_module_lessons l diff --git a/db/query/exam_prep_unit_modules.sql b/db/query/exam_prep_unit_modules.sql index 41703e6..0a5299c 100644 --- a/db/query/exam_prep_unit_modules.sql +++ b/db/query/exam_prep_unit_modules.sql @@ -16,9 +16,16 @@ RETURNING *; -- name: ExamPrepGetUnitModuleByID :one -SELECT * -FROM exam_prep.unit_modules -WHERE id = $1; +SELECT + m.*, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + WHERE l.unit_module_id = m.id + ) AS has_practice +FROM exam_prep.unit_modules m +WHERE m.id = $1; -- name: ExamPrepListUnitModuleIDsByUnit :many SELECT @@ -51,6 +58,7 @@ SELECT m.sort_order, COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(mc.practices_count, 0)::BIGINT AS practices_count, + (COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice, m.created_at, m.updated_at FROM exam_prep.unit_modules m diff --git a/db/query/exam_prep_units.sql b/db/query/exam_prep_units.sql index 98bb082..a30d162 100644 --- a/db/query/exam_prep_units.sql +++ b/db/query/exam_prep_units.sql @@ -15,9 +15,17 @@ RETURNING *; -- name: ExamPrepGetUnitByID :one -SELECT * -FROM exam_prep.units -WHERE id = $1; +SELECT + u.*, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + WHERE m.unit_id = u.id + ) AS has_practice +FROM exam_prep.units u +WHERE u.id = $1; -- name: ExamPrepListUnitIDsByCatalogCourse :many SELECT @@ -52,6 +60,7 @@ SELECT COALESCE(uc.modules_count, 0)::BIGINT AS modules_count, COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(uc.practices_count, 0)::BIGINT AS practices_count, + (COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice, u.created_at, u.updated_at FROM exam_prep.units u diff --git a/db/query/lms_courses.sql b/db/query/lms_courses.sql index e3cccbf..7f8d612 100644 --- a/db/query/lms_courses.sql +++ b/db/query/lms_courses.sql @@ -15,9 +15,18 @@ RETURNING *; -- name: GetCourseByID :one -SELECT * +SELECT + c.*, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.course_id = c.id + AND p.module_id IS NULL + AND p.lesson_id IS NULL + ) AS has_practice FROM courses -WHERE id = $1; + c +WHERE c.id = $1; -- name: ListCourseIDsByProgram :many SELECT @@ -65,7 +74,14 @@ SELECT WHERE p.course_id = c.id AND p.module_id IS NULL - AND p.lesson_id IS NULL) AS practice_count + AND p.lesson_id IS NULL) AS practice_count, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.course_id = c.id + AND p.module_id IS NULL + AND p.lesson_id IS NULL + ) AS has_practice FROM courses c WHERE diff --git a/db/query/lms_lessons.sql b/db/query/lms_lessons.sql index e306013..011adbd 100644 --- a/db/query/lms_lessons.sql +++ b/db/query/lms_lessons.sql @@ -16,9 +16,16 @@ RETURNING *; -- name: GetLessonByID :one -SELECT * +SELECT + l.*, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.lesson_id = l.id + ) AS has_practice FROM lessons -WHERE id = $1; + l +WHERE l.id = $1; -- name: ListLessonsByModuleID :many SELECT @@ -31,7 +38,12 @@ SELECT l.description, l.sort_order, l.created_at, - l.updated_at + l.updated_at, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.lesson_id = l.id + ) AS has_practice FROM lessons l WHERE diff --git a/db/query/lms_modules.sql b/db/query/lms_modules.sql index a389d56..a149913 100644 --- a/db/query/lms_modules.sql +++ b/db/query/lms_modules.sql @@ -16,9 +16,17 @@ RETURNING *; -- name: GetModuleByID :one -SELECT * +SELECT + m.*, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.module_id = m.id + AND p.lesson_id IS NULL + ) AS has_practice FROM modules -WHERE id = $1; + m +WHERE m.id = $1; -- name: ListModuleIDsByCourse :many SELECT @@ -41,7 +49,13 @@ SELECT m.icon, m.sort_order, m.created_at, - m.updated_at + m.updated_at, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.module_id = m.id + AND p.lesson_id IS NULL + ) AS has_practice FROM modules m WHERE diff --git a/docs/docs.go b/docs/docs.go index 14c36a9..ca0a090 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -9405,6 +9405,60 @@ const docTemplate = `{ } } }, + "domain.DynamicElementDefinition": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" + } + } + }, + "domain.DynamicElementInstance": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "meta": { + "type": "object", + "additionalProperties": true + }, + "value": {} + } + }, + "domain.DynamicQuestionPayload": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementInstance" + } + }, + "stimulus": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementInstance" + } + } + } + }, "domain.EmploymentType": { "type": "string", "enum": [ @@ -9638,6 +9692,9 @@ const docTemplate = `{ "difficultyLevel": { "type": "string" }, + "dynamicPayload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -10927,6 +10984,9 @@ const docTemplate = `{ "difficulty_level": { "type": "string" }, + "dynamic_payload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -11046,6 +11106,12 @@ const docTemplate = `{ "type": "string" } }, + "response_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } + }, "status": { "type": "string" }, @@ -11054,6 +11120,12 @@ const docTemplate = `{ "items": { "type": "string" } + }, + "stimulus_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } } } }, @@ -11409,6 +11481,9 @@ const docTemplate = `{ "difficulty_level": { "type": "string" }, + "dynamic_payload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -11500,6 +11575,12 @@ const docTemplate = `{ "type": "string" } }, + "response_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } + }, "status": { "type": "string" }, @@ -11508,6 +11589,12 @@ const docTemplate = `{ "items": { "type": "string" } + }, + "stimulus_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 6381314..bbd1698 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -9397,6 +9397,60 @@ } } }, + "domain.DynamicElementDefinition": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" + } + } + }, + "domain.DynamicElementInstance": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "meta": { + "type": "object", + "additionalProperties": true + }, + "value": {} + } + }, + "domain.DynamicQuestionPayload": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementInstance" + } + }, + "stimulus": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementInstance" + } + } + } + }, "domain.EmploymentType": { "type": "string", "enum": [ @@ -9630,6 +9684,9 @@ "difficultyLevel": { "type": "string" }, + "dynamicPayload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -10919,6 +10976,9 @@ "difficulty_level": { "type": "string" }, + "dynamic_payload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -11038,6 +11098,12 @@ "type": "string" } }, + "response_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } + }, "status": { "type": "string" }, @@ -11046,6 +11112,12 @@ "items": { "type": "string" } + }, + "stimulus_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } } } }, @@ -11401,6 +11473,9 @@ "difficulty_level": { "type": "string" }, + "dynamic_payload": { + "$ref": "#/definitions/domain.DynamicQuestionPayload" + }, "explanation": { "type": "string" }, @@ -11492,6 +11567,12 @@ "type": "string" } }, + "response_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } + }, "status": { "type": "string" }, @@ -11500,6 +11581,12 @@ "items": { "type": "string" } + }, + "stimulus_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DynamicElementDefinition" + } } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 072aac3..ca526e3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -212,6 +212,42 @@ definitions: - password - team_role type: object + domain.DynamicElementDefinition: + properties: + config: + additionalProperties: true + type: object + id: + type: string + kind: + type: string + label: + type: string + required: + type: boolean + type: object + domain.DynamicElementInstance: + properties: + id: + type: string + kind: + type: string + meta: + additionalProperties: true + type: object + value: {} + type: object + domain.DynamicQuestionPayload: + properties: + response: + items: + $ref: '#/definitions/domain.DynamicElementInstance' + type: array + stimulus: + items: + $ref: '#/definitions/domain.DynamicElementInstance' + type: array + type: object domain.EmploymentType: enum: - full_time @@ -371,6 +407,8 @@ definitions: type: string difficultyLevel: type: string + dynamicPayload: + $ref: '#/definitions/domain.DynamicQuestionPayload' explanation: type: string id: @@ -1240,6 +1278,8 @@ definitions: type: string difficulty_level: type: string + dynamic_payload: + $ref: '#/definitions/domain.DynamicQuestionPayload' explanation: type: string image_url: @@ -1320,12 +1360,20 @@ definitions: items: type: string type: array + response_schema: + items: + $ref: '#/definitions/domain.DynamicElementDefinition' + type: array status: type: string stimulus_component_kinds: items: type: string type: array + stimulus_schema: + items: + $ref: '#/definitions/domain.DynamicElementDefinition' + type: array required: - display_name - key @@ -1567,6 +1615,8 @@ definitions: type: string difficulty_level: type: string + dynamic_payload: + $ref: '#/definitions/domain.DynamicQuestionPayload' explanation: type: string image_url: @@ -1627,12 +1677,20 @@ definitions: items: type: string type: array + response_schema: + items: + $ref: '#/definitions/domain.DynamicElementDefinition' + type: array status: type: string stimulus_component_kinds: items: type: string type: array + stimulus_schema: + items: + $ref: '#/definitions/domain.DynamicElementDefinition' + type: array type: object handlers.validateQuestionTypeDefinitionReq: properties: diff --git a/gen/db/exam_prep_catalog_courses.sql.go b/gen/db/exam_prep_catalog_courses.sql.go index ac6b982..0bc2705 100644 --- a/gen/db/exam_prep_catalog_courses.sql.go +++ b/gen/db/exam_prep_catalog_courses.sql.go @@ -57,14 +57,34 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err } const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one -SELECT id, name, description, thumbnail, sort_order, created_at, updated_at -FROM exam_prep.catalog_courses -WHERE id = $1 +SELECT + c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + WHERE u.catalog_course_id = c.id + ) AS has_practice +FROM exam_prep.catalog_courses c +WHERE c.id = $1 ` -func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepCatalogCourse, error) { +type ExamPrepGetCatalogCourseByIDRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + SortOrder int32 `json:"sort_order"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepGetCatalogCourseByIDRow, error) { row := q.db.QueryRow(ctx, ExamPrepGetCatalogCourseByID, id) - var i ExamPrepCatalogCourse + var i ExamPrepGetCatalogCourseByIDRow err := row.Scan( &i.ID, &i.Name, @@ -73,6 +93,7 @@ func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (E &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ) return i, err } @@ -126,6 +147,14 @@ SELECT COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + WHERE u.catalog_course_id = c.id + ) AS has_practice, c.created_at, c.updated_at FROM exam_prep.catalog_courses c @@ -149,6 +178,7 @@ type ExamPrepListCatalogCoursesRow struct { UnitsCount int64 `json:"units_count"` ModulesCount int64 `json:"modules_count"` LessonsCount int64 `json:"lessons_count"` + HasPractice bool `json:"has_practice"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } @@ -172,6 +202,7 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi &i.UnitsCount, &i.ModulesCount, &i.LessonsCount, + &i.HasPractice, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/gen/db/exam_prep_unit_module_lessons.sql.go b/gen/db/exam_prep_unit_module_lessons.sql.go index 62f346a..62a81ea 100644 --- a/gen/db/exam_prep_unit_module_lessons.sql.go +++ b/gen/db/exam_prep_unit_module_lessons.sql.go @@ -71,14 +71,33 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64) } const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one -SELECT id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at -FROM exam_prep.unit_module_lessons -WHERE id = $1 +SELECT + l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + WHERE p.unit_module_lesson_id = l.id + ) AS has_practice +FROM exam_prep.unit_module_lessons l +WHERE l.id = $1 ` -func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepUnitModuleLesson, error) { +type ExamPrepGetUnitModuleLessonByIDRow struct { + ID int64 `json:"id"` + UnitModuleID int64 `json:"unit_module_id"` + Title string `json:"title"` + VideoUrl pgtype.Text `json:"video_url"` + Thumbnail pgtype.Text `json:"thumbnail"` + Description pgtype.Text `json:"description"` + SortOrder int32 `json:"sort_order"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleLessonByIDRow, error) { row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleLessonByID, id) - var i ExamPrepUnitModuleLesson + var i ExamPrepGetUnitModuleLessonByIDRow err := row.Scan( &i.ID, &i.UnitModuleID, @@ -89,6 +108,7 @@ func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ) return i, err } @@ -133,6 +153,11 @@ SELECT l.thumbnail, l.description, l.sort_order, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + WHERE p.unit_module_lesson_id = l.id + ) AS has_practice, l.created_at, l.updated_at FROM exam_prep.unit_module_lessons l @@ -160,6 +185,7 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct { Thumbnail pgtype.Text `json:"thumbnail"` Description pgtype.Text `json:"description"` SortOrder int32 `json:"sort_order"` + HasPractice bool `json:"has_practice"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } @@ -182,6 +208,7 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex &i.Thumbnail, &i.Description, &i.SortOrder, + &i.HasPractice, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/gen/db/exam_prep_unit_modules.sql.go b/gen/db/exam_prep_unit_modules.sql.go index 9b50700..8a767b2 100644 --- a/gen/db/exam_prep_unit_modules.sql.go +++ b/gen/db/exam_prep_unit_modules.sql.go @@ -71,14 +71,34 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error } const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one -SELECT id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at -FROM exam_prep.unit_modules -WHERE id = $1 +SELECT + m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + WHERE l.unit_module_id = m.id + ) AS has_practice +FROM exam_prep.unit_modules m +WHERE m.id = $1 ` -func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepUnitModule, error) { +type ExamPrepGetUnitModuleByIDRow struct { + ID int64 `json:"id"` + UnitID int64 `json:"unit_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + Icon pgtype.Text `json:"icon"` + SortOrder int32 `json:"sort_order"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleByIDRow, error) { row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleByID, id) - var i ExamPrepUnitModule + var i ExamPrepGetUnitModuleByIDRow err := row.Scan( &i.ID, &i.UnitID, @@ -89,6 +109,7 @@ func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (Exam &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ) return i, err } @@ -145,6 +166,7 @@ SELECT m.sort_order, COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(mc.practices_count, 0)::BIGINT AS practices_count, + (COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice, m.created_at, m.updated_at FROM exam_prep.unit_modules m @@ -175,6 +197,7 @@ type ExamPrepListUnitModulesByUnitRow struct { SortOrder int32 `json:"sort_order"` LessonsCount int64 `json:"lessons_count"` PracticesCount int64 `json:"practices_count"` + HasPractice bool `json:"has_practice"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } @@ -199,6 +222,7 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre &i.SortOrder, &i.LessonsCount, &i.PracticesCount, + &i.HasPractice, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/gen/db/exam_prep_units.sql.go b/gen/db/exam_prep_units.sql.go index b64b422..3bb5014 100644 --- a/gen/db/exam_prep_units.sql.go +++ b/gen/db/exam_prep_units.sql.go @@ -67,14 +67,34 @@ func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error { } const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one -SELECT id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at -FROM exam_prep.units -WHERE id = $1 +SELECT + u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at, + EXISTS ( + SELECT 1 + FROM exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + WHERE m.unit_id = u.id + ) AS has_practice +FROM exam_prep.units u +WHERE u.id = $1 ` -func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepUnit, error) { +type ExamPrepGetUnitByIDRow struct { + ID int64 `json:"id"` + CatalogCourseID int64 `json:"catalog_course_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + SortOrder int32 `json:"sort_order"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepGetUnitByIDRow, error) { row := q.db.QueryRow(ctx, ExamPrepGetUnitByID, id) - var i ExamPrepUnit + var i ExamPrepGetUnitByIDRow err := row.Scan( &i.ID, &i.CatalogCourseID, @@ -84,6 +104,7 @@ func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepUn &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ) return i, err } @@ -142,6 +163,7 @@ SELECT COALESCE(uc.modules_count, 0)::BIGINT AS modules_count, COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(uc.practices_count, 0)::BIGINT AS practices_count, + (COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice, u.created_at, u.updated_at FROM exam_prep.units u @@ -172,6 +194,7 @@ type ExamPrepListUnitsByCatalogCourseRow struct { ModulesCount int64 `json:"modules_count"` LessonsCount int64 `json:"lessons_count"` PracticesCount int64 `json:"practices_count"` + HasPractice bool `json:"has_practice"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } @@ -196,6 +219,7 @@ func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg Exam &i.ModulesCount, &i.LessonsCount, &i.PracticesCount, + &i.HasPractice, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/gen/db/lms_courses.sql.go b/gen/db/lms_courses.sql.go index 6f8b400..3d96a13 100644 --- a/gen/db/lms_courses.sql.go +++ b/gen/db/lms_courses.sql.go @@ -67,14 +67,35 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error { } const GetCourseByID = `-- name: GetCourseByID :one -SELECT id, program_id, name, description, thumbnail, created_at, updated_at, sort_order +SELECT + c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.course_id = c.id + AND p.module_id IS NULL + AND p.lesson_id IS NULL + ) AS has_practice FROM courses -WHERE id = $1 + c +WHERE c.id = $1 ` -func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) { +type GetCourseByIDRow struct { + ID int64 `json:"id"` + ProgramID int64 `json:"program_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + SortOrder int32 `json:"sort_order"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow, error) { row := q.db.QueryRow(ctx, GetCourseByID, id) - var i Course + var i GetCourseByIDRow err := row.Scan( &i.ID, &i.ProgramID, @@ -84,6 +105,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) { &i.CreatedAt, &i.UpdatedAt, &i.SortOrder, + &i.HasPractice, ) return i, err } @@ -155,7 +177,14 @@ SELECT WHERE p.course_id = c.id AND p.module_id IS NULL - AND p.lesson_id IS NULL) AS practice_count + AND p.lesson_id IS NULL) AS practice_count, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.course_id = c.id + AND p.module_id IS NULL + AND p.lesson_id IS NULL + ) AS has_practice FROM courses c WHERE @@ -185,6 +214,7 @@ type ListCoursesByProgramIDRow struct { ModuleCount int64 `json:"module_count"` LessonCount int64 `json:"lesson_count"` PracticeCount int64 `json:"practice_count"` + HasPractice bool `json:"has_practice"` } func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) { @@ -209,6 +239,7 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP &i.ModuleCount, &i.LessonCount, &i.PracticeCount, + &i.HasPractice, ); err != nil { return nil, err } diff --git a/gen/db/lms_lessons.sql.go b/gen/db/lms_lessons.sql.go index 48ac1d4..ece62af 100644 --- a/gen/db/lms_lessons.sql.go +++ b/gen/db/lms_lessons.sql.go @@ -71,14 +71,34 @@ func (q *Queries) DeleteLesson(ctx context.Context, id int64) error { } const GetLessonByID = `-- name: GetLessonByID :one -SELECT id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order +SELECT + l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.lesson_id = l.id + ) AS has_practice FROM lessons -WHERE id = $1 + l +WHERE l.id = $1 ` -func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) { +type GetLessonByIDRow struct { + ID int64 `json:"id"` + ModuleID int64 `json:"module_id"` + Title string `json:"title"` + VideoUrl pgtype.Text `json:"video_url"` + Thumbnail pgtype.Text `json:"thumbnail"` + Description pgtype.Text `json:"description"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + SortOrder int32 `json:"sort_order"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) GetLessonByID(ctx context.Context, id int64) (GetLessonByIDRow, error) { row := q.db.QueryRow(ctx, GetLessonByID, id) - var i Lesson + var i GetLessonByIDRow err := row.Scan( &i.ID, &i.ModuleID, @@ -89,6 +109,7 @@ func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) { &i.CreatedAt, &i.UpdatedAt, &i.SortOrder, + &i.HasPractice, ) return i, err } @@ -104,7 +125,12 @@ SELECT l.description, l.sort_order, l.created_at, - l.updated_at + l.updated_at, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.lesson_id = l.id + ) AS has_practice FROM lessons l WHERE @@ -133,6 +159,7 @@ type ListLessonsByModuleIDRow struct { SortOrder int32 `json:"sort_order"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` } func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) { @@ -155,6 +182,7 @@ func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByMo &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ); err != nil { return nil, err } diff --git a/gen/db/lms_modules.sql.go b/gen/db/lms_modules.sql.go index a019c71..e7e6285 100644 --- a/gen/db/lms_modules.sql.go +++ b/gen/db/lms_modules.sql.go @@ -71,14 +71,35 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error { } const GetModuleByID = `-- name: GetModuleByID :one -SELECT id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order +SELECT + m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.module_id = m.id + AND p.lesson_id IS NULL + ) AS has_practice FROM modules -WHERE id = $1 + m +WHERE m.id = $1 ` -func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) { +type GetModuleByIDRow struct { + ID int64 `json:"id"` + ProgramID int64 `json:"program_id"` + CourseID int64 `json:"course_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + Icon pgtype.Text `json:"icon"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + SortOrder int32 `json:"sort_order"` + HasPractice bool `json:"has_practice"` +} + +func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow, error) { row := q.db.QueryRow(ctx, GetModuleByID, id) - var i Module + var i GetModuleByIDRow err := row.Scan( &i.ID, &i.ProgramID, @@ -89,6 +110,7 @@ func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) { &i.CreatedAt, &i.UpdatedAt, &i.SortOrder, + &i.HasPractice, ) return i, err } @@ -135,7 +157,13 @@ SELECT m.icon, m.sort_order, m.created_at, - m.updated_at + m.updated_at, + EXISTS ( + SELECT 1 + FROM lms_practices p + WHERE p.module_id = m.id + AND p.lesson_id IS NULL + ) AS has_practice FROM modules m WHERE @@ -166,6 +194,7 @@ type ListModulesByProgramAndCourseRow struct { SortOrder int32 `json:"sort_order"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + HasPractice bool `json:"has_practice"` } func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) { @@ -193,6 +222,7 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod &i.SortOrder, &i.CreatedAt, &i.UpdatedAt, + &i.HasPractice, ); err != nil { return nil, err } diff --git a/internal/domain/course.go b/internal/domain/course.go index 0a00dcd..541f91f 100644 --- a/internal/domain/course.go +++ b/internal/domain/course.go @@ -28,6 +28,7 @@ type Course struct { ModuleCount int `json:"module_count"` LessonCount int `json:"lesson_count"` PracticeCount int `json:"practice_count"` + HasPractice bool `json:"has_practice"` Access *LMSEntityAccess `json:"access,omitempty"` } diff --git a/internal/domain/exam_prep_catalog_course.go b/internal/domain/exam_prep_catalog_course.go index e0d6aaa..4384c49 100644 --- a/internal/domain/exam_prep_catalog_course.go +++ b/internal/domain/exam_prep_catalog_course.go @@ -12,6 +12,7 @@ type ExamPrepCatalogCourse struct { UnitsCount *int64 `json:"units_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_lesson.go b/internal/domain/exam_prep_lesson.go index 069d767..841b7ab 100644 --- a/internal/domain/exam_prep_lesson.go +++ b/internal/domain/exam_prep_lesson.go @@ -11,6 +11,7 @@ type ExamPrepLesson struct { Thumbnail *string `json:"thumbnail,omitempty"` Description *string `json:"description,omitempty"` SortOrder int `json:"sort_order"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_module.go b/internal/domain/exam_prep_module.go index d6f60dc..0b12a03 100644 --- a/internal/domain/exam_prep_module.go +++ b/internal/domain/exam_prep_module.go @@ -13,6 +13,7 @@ type ExamPrepModule struct { SortOrder int `json:"sort_order"` LessonsCount *int64 `json:"lessons_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_unit.go b/internal/domain/exam_prep_unit.go index 4ef6041..8510438 100644 --- a/internal/domain/exam_prep_unit.go +++ b/internal/domain/exam_prep_unit.go @@ -13,6 +13,7 @@ type ExamPrepUnit struct { ModulesCount *int64 `json:"modules_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/lesson.go b/internal/domain/lesson.go index 4f174b1..7f9c450 100644 --- a/internal/domain/lesson.go +++ b/internal/domain/lesson.go @@ -11,6 +11,7 @@ type Lesson struct { Thumbnail *string `json:"thumbnail,omitempty"` Description *string `json:"description,omitempty"` SortOrder int `json:"sort_order"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"` diff --git a/internal/domain/module.go b/internal/domain/module.go index 32f116d..769fa64 100644 --- a/internal/domain/module.go +++ b/internal/domain/module.go @@ -11,6 +11,7 @@ type Module struct { Description *string `json:"description,omitempty"` Icon *string `json:"icon,omitempty"` SortOrder int `json:"sort_order"` + HasPractice bool `json:"has_practice"` CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"` diff --git a/internal/repository/exam_prep_catalog_courses.go b/internal/repository/exam_prep_catalog_courses.go index 1ae5cd8..903d98a 100644 --- a/internal/repository/exam_prep_catalog_courses.go +++ b/internal/repository/exam_prep_catalog_courses.go @@ -47,7 +47,17 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom } return domain.ExamPrepCatalogCourse{}, err } - return examPrepCatalogCourseToDomain(c), nil + out := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{ + ID: c.ID, + Name: c.Name, + Description: c.Description, + Thumbnail: c.Thumbnail, + SortOrder: c.SortOrder, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + }) + out.HasPractice = c.HasPractice + return out, nil } func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) { @@ -79,6 +89,7 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset in item.UnitsCount = &r.UnitsCount item.ModulesCount = &r.ModulesCount item.LessonsCount = &r.LessonsCount + item.HasPractice = r.HasPractice out = append(out, item) } return out, total, nil diff --git a/internal/repository/exam_prep_unit_module_lessons.go b/internal/repository/exam_prep_unit_module_lessons.go index 3f5088f..facdbb6 100644 --- a/internal/repository/exam_prep_unit_module_lessons.go +++ b/internal/repository/exam_prep_unit_module_lessons.go @@ -51,7 +51,19 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) ( } return domain.ExamPrepLesson{}, err } - return examPrepLessonToDomain(l), nil + out := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ + ID: l.ID, + UnitModuleID: l.UnitModuleID, + Title: l.Title, + VideoUrl: l.VideoUrl, + Thumbnail: l.Thumbnail, + Description: l.Description, + SortOrder: l.SortOrder, + CreatedAt: l.CreatedAt, + UpdatedAt: l.UpdatedAt, + }) + out.HasPractice = l.HasPractice + return out, nil } func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) { @@ -72,7 +84,7 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, if i == 0 { total = r.TotalCount } - out = append(out, examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ + item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ ID: r.ID, UnitModuleID: r.UnitModuleID, Title: r.Title, @@ -82,7 +94,9 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, SortOrder: r.SortOrder, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, - })) + }) + item.HasPractice = r.HasPractice + out = append(out, item) } return out, total, nil } diff --git a/internal/repository/exam_prep_unit_modules.go b/internal/repository/exam_prep_unit_modules.go index e3001e1..93af321 100644 --- a/internal/repository/exam_prep_unit_modules.go +++ b/internal/repository/exam_prep_unit_modules.go @@ -51,7 +51,19 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain } return domain.ExamPrepModule{}, err } - return examPrepModuleToDomain(m), nil + out := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{ + ID: m.ID, + UnitID: m.UnitID, + Name: m.Name, + Description: m.Description, + Thumbnail: m.Thumbnail, + Icon: m.Icon, + SortOrder: m.SortOrder, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + }) + out.HasPractice = m.HasPractice + return out, nil } func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) { @@ -85,6 +97,7 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, }) item.LessonsCount = &r.LessonsCount item.PracticesCount = &r.PracticesCount + item.HasPractice = r.HasPractice out = append(out, item) } return out, total, nil diff --git a/internal/repository/exam_prep_units.go b/internal/repository/exam_prep_units.go index ebdd83f..296dd01 100644 --- a/internal/repository/exam_prep_units.go +++ b/internal/repository/exam_prep_units.go @@ -49,7 +49,18 @@ func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamP } return domain.ExamPrepUnit{}, err } - return examPrepUnitToDomain(u), nil + out := examPrepUnitToDomain(dbgen.ExamPrepUnit{ + ID: u.ID, + CatalogCourseID: u.CatalogCourseID, + Name: u.Name, + Description: u.Description, + Thumbnail: u.Thumbnail, + SortOrder: u.SortOrder, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + }) + out.HasPractice = u.HasPractice + return out, nil } func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) { @@ -83,6 +94,7 @@ func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCou item.ModulesCount = &r.ModulesCount item.LessonsCount = &r.LessonsCount item.PracticesCount = &r.PracticesCount + item.HasPractice = r.HasPractice out = append(out, item) } return out, total, nil diff --git a/internal/repository/lms_courses.go b/internal/repository/lms_courses.go index 514dc56..bcd47c7 100644 --- a/internal/repository/lms_courses.go +++ b/internal/repository/lms_courses.go @@ -53,7 +53,18 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err } return domain.Course{}, err } - return courseToDomain(c), nil + out := courseToDomain(dbgen.Course{ + ID: c.ID, + ProgramID: c.ProgramID, + Name: c.Name, + Description: c.Description, + Thumbnail: c.Thumbnail, + SortOrder: c.SortOrder, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + }) + out.HasPractice = c.HasPractice + return out, nil } func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) { @@ -87,6 +98,7 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim co.ModuleCount = int(r.ModuleCount) co.LessonCount = int(r.LessonCount) co.PracticeCount = int(r.PracticeCount) + co.HasPractice = r.HasPractice out = append(out, co) } return out, total, nil diff --git a/internal/repository/lms_lessons.go b/internal/repository/lms_lessons.go index 9c4f106..9cb490c 100644 --- a/internal/repository/lms_lessons.go +++ b/internal/repository/lms_lessons.go @@ -51,7 +51,19 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err } return domain.Lesson{}, err } - return lessonToDomain(l), nil + out := lessonToDomain(dbgen.Lesson{ + ID: l.ID, + ModuleID: l.ModuleID, + Title: l.Title, + VideoUrl: l.VideoUrl, + Thumbnail: l.Thumbnail, + Description: l.Description, + SortOrder: l.SortOrder, + CreatedAt: l.CreatedAt, + UpdatedAt: l.UpdatedAt, + }) + out.HasPractice = l.HasPractice + return out, nil } func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) { @@ -72,7 +84,7 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit if i == 0 { total = r.TotalCount } - out = append(out, lessonToDomain(dbgen.Lesson{ + lesson := lessonToDomain(dbgen.Lesson{ ID: r.ID, ModuleID: r.ModuleID, Title: r.Title, @@ -82,7 +94,9 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, SortOrder: r.SortOrder, - })) + }) + lesson.HasPractice = r.HasPractice + out = append(out, lesson) } return out, total, nil } diff --git a/internal/repository/lms_modules.go b/internal/repository/lms_modules.go index 2a59390..24c33df 100644 --- a/internal/repository/lms_modules.go +++ b/internal/repository/lms_modules.go @@ -55,7 +55,19 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err } return domain.Module{}, err } - return moduleToDomain(m), nil + out := moduleToDomain(dbgen.Module{ + ID: m.ID, + ProgramID: m.ProgramID, + CourseID: m.CourseID, + Name: m.Name, + Description: m.Description, + Icon: m.Icon, + SortOrder: m.SortOrder, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + }) + out.HasPractice = m.HasPractice + return out, nil } func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) { @@ -77,7 +89,7 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co if i == 0 { total = r.TotalCount } - out = append(out, moduleToDomain(dbgen.Module{ + mod := moduleToDomain(dbgen.Module{ ID: r.ID, ProgramID: r.ProgramID, CourseID: r.CourseID, @@ -87,7 +99,9 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, SortOrder: r.SortOrder, - })) + }) + mod.HasPractice = r.HasPractice + out = append(out, mod) } return out, total, nil } diff --git a/internal/services/messenger/email.go b/internal/services/messenger/email.go index c6df6a2..7b9058b 100644 --- a/internal/services/messenger/email.go +++ b/internal/services/messenger/email.go @@ -12,7 +12,7 @@ func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, func (s *Service) SendEmailWithAttachments(ctx context.Context, receiverEmail, message string, messageHTML string, subject string, attachments []*resend.Attachment) error { apiKey := s.config.ResendApiKey client := resend.NewClient(apiKey) - formattedSenderEmail := "Y <" + s.config.ResendSenderEmail + ">" + formattedSenderEmail := "Yimaru - Academy <" + s.config.ResendSenderEmail + ">" params := &resend.SendEmailRequest{ From: formattedSenderEmail, To: []string{receiverEmail}, diff --git a/internal/web_server/handlers/question_type_builder.go b/internal/web_server/handlers/question_type_builder.go index 3b411b8..d1a5af2 100644 --- a/internal/web_server/handlers/question_type_builder.go +++ b/internal/web_server/handlers/question_type_builder.go @@ -2,6 +2,9 @@ package handlers import ( "Yimaru-Backend/internal/domain" + "context" + "encoding/json" + "fmt" "strconv" "strings" @@ -132,6 +135,27 @@ func (h *Handler) CreateQuestionTypeDefinition(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_type_definition_id": def.ID, + "key": def.Key, + }) + go h.activityLogSvc.RecordAction( + context.Background(), + &actorID, + &actorRole, + domain.ActionQuestionCreated, + domain.ResourceQuestion, + &def.ID, + "Created question type definition: "+def.DisplayName, + meta, + &ip, + &ua, + ) + return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Question type definition created", Data: def, @@ -251,6 +275,26 @@ func (h *Handler) UpdateQuestionTypeDefinition(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_type_definition_id": id, + }) + go h.activityLogSvc.RecordAction( + context.Background(), + &actorID, + &actorRole, + domain.ActionQuestionUpdated, + domain.ResourceQuestion, + &id, + fmt.Sprintf("Updated question type definition ID: %d", id), + meta, + &ip, + &ua, + ) + return c.JSON(domain.Response{ Message: "Question type definition updated", Data: fiber.Map{"id": id}, @@ -282,6 +326,26 @@ func (h *Handler) DeleteQuestionTypeDefinition(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_type_definition_id": id, + }) + go h.activityLogSvc.RecordAction( + context.Background(), + &actorID, + &actorRole, + domain.ActionQuestionDeleted, + domain.ResourceQuestion, + &id, + fmt.Sprintf("Deleted question type definition ID: %d", id), + meta, + &ip, + &ua, + ) + return c.JSON(domain.Response{ Message: "Question type definition deleted", Data: fiber.Map{"id": id}, diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index 58552d0..a7f115f 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -1299,6 +1299,17 @@ func (h *Handler) AddQuestionToSet(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_set_id": setID, + "question_id": req.QuestionID, + "display_order": req.DisplayOrder, + }) + go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Added question %d to question set %d", req.QuestionID, setID), meta, &ip, &ua) + return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Question added to set successfully", Data: map[string]interface{}{ @@ -1605,6 +1616,16 @@ func (h *Handler) RemoveQuestionFromSet(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_set_id": setID, + "question_id": questionID, + }) + go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Removed question %d from question set %d", questionID, setID), meta, &ip, &ua) + return c.JSON(domain.Response{ Message: "Question removed from set successfully", }) @@ -1662,6 +1683,17 @@ func (h *Handler) UpdateQuestionOrderInSet(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_set_id": setID, + "question_id": questionID, + "display_order": req.DisplayOrder, + }) + go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Updated question %d display_order to %d in set %d", questionID, req.DisplayOrder, setID), meta, &ip, &ua) + return c.JSON(domain.Response{ Message: "Question order updated successfully", }) @@ -1769,6 +1801,17 @@ func (h *Handler) AddUserPersonaToQuestionSet(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_set_id": setID, + "user_id": req.UserID, + "display_order": req.DisplayOrder, + }) + go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Added persona user %d to question set %d", req.UserID, setID), meta, &ip, &ua) + return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Persona added to question set successfully", }) @@ -1812,6 +1855,16 @@ func (h *Handler) RemoveUserPersonaFromQuestionSet(c *fiber.Ctx) error { }) } + actorID := c.Locals("user_id").(int64) + actorRole := string(c.Locals("role").(domain.Role)) + ip := c.IP() + ua := c.Get("User-Agent") + meta, _ := json.Marshal(map[string]interface{}{ + "question_set_id": setID, + "user_id": userID, + }) + go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Removed persona user %d from question set %d", userID, setID), meta, &ip, &ua) + return c.JSON(domain.Response{ Message: "Persona removed from question set successfully", }) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index bc0d601..3f9c428 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -309,7 +309,19 @@ func (a *App) initAppRoutes() { groupV1.Post("/notifications/mark-all-unread", a.authMiddleware, a.RequirePermission("notifications.mark_all_unread"), h.MarkAllNotificationsAsUnread) groupV1.Delete("/notifications", a.authMiddleware, a.RequirePermission("notifications.delete_mine"), h.DeleteUserNotifications) groupV1.Get("/notifications/unread", a.authMiddleware, a.RequirePermission("notifications.count_unread"), h.CountUnreadNotifications) - groupV1.Post("/notifications/create", a.authMiddleware, a.RequirePermission("notifications.create"), h.CreateAndSendNotification) + groupV1.Post("/notifications/create", a.authMiddleware, a.RequirePermission("notifications.create"), h.CreateAndSendNotification) + groupV1.Post("/notifications/test-push", a.authMiddleware, a.RequirePermission("notifications.test_push"), h.SendTestPushNotification) + + // Bulk Notifications + groupV1.Post("/notifications/bulk-push", a.authMiddleware, a.RequirePermission("notifications.bulk_push"), h.SendBulkPushNotification) + groupV1.Post("/notifications/bulk-sms", a.authMiddleware, a.RequirePermission("notifications.bulk_sms"), h.SendBulkSMS) + groupV1.Post("/notifications/send-email", a.authMiddleware, a.RequirePermission("notifications.send_email"), h.SendSingleEmail) + groupV1.Post("/notifications/bulk-email", a.authMiddleware, a.RequirePermission("notifications.bulk_email"), h.SendBulkEmail) + + // Scheduled Notifications + groupV1.Get("/notifications/scheduled", a.authMiddleware, a.RequirePermission("notifications_scheduled.list"), h.ListScheduledNotifications) + groupV1.Get("/notifications/scheduled/:id", a.authMiddleware, a.RequirePermission("notifications_scheduled.get"), h.GetScheduledNotification) + groupV1.Post("/notifications/scheduled/:id/cancel", a.authMiddleware, a.RequirePermission("notifications_scheduled.cancel"), h.CancelScheduledNotification) // Issues groupV1.Post("/issues", a.authMiddleware, a.RequirePermission("issues.create"), h.CreateIssue) @@ -324,18 +336,6 @@ func (a *App) initAppRoutes() { groupV1.Post("/devices/register", a.authMiddleware, a.RequirePermission("devices.register"), h.RegisterDeviceToken) groupV1.Post("/devices/unregister", a.authMiddleware, a.RequirePermission("devices.unregister"), h.UnregisterDeviceToken) - // Push Notifications - groupV1.Post("/notifications/test-push", a.authMiddleware, a.RequirePermission("notifications.test_push"), h.SendTestPushNotification) - groupV1.Post("/notifications/bulk-push", a.authMiddleware, a.RequirePermission("notifications.bulk_push"), h.SendBulkPushNotification) - groupV1.Post("/notifications/bulk-sms", a.authMiddleware, a.RequirePermission("notifications.bulk_sms"), h.SendBulkSMS) - groupV1.Post("/notifications/send-email", a.authMiddleware, a.RequirePermission("notifications.send_email"), h.SendSingleEmail) - groupV1.Post("/notifications/bulk-email", a.authMiddleware, a.RequirePermission("notifications.bulk_email"), h.SendBulkEmail) - - // Scheduled Notifications - groupV1.Get("/notifications/scheduled", a.authMiddleware, a.RequirePermission("notifications_scheduled.list"), h.ListScheduledNotifications) - groupV1.Get("/notifications/scheduled/:id", a.authMiddleware, a.RequirePermission("notifications_scheduled.get"), h.GetScheduledNotification) - groupV1.Post("/notifications/scheduled/:id/cancel", a.authMiddleware, a.RequirePermission("notifications_scheduled.cancel"), h.CancelScheduledNotification) - // Settings groupV1.Get("/settings", a.authMiddleware, a.RequirePermission("settings.list"), h.GetGlobalSettingList) groupV1.Get("/settings/:key", a.authMiddleware, a.RequirePermission("settings.get"), h.GetGlobalSettingByKey)