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 <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-08 11:57:11 -07:00
parent 9da9eb77e5
commit bc2357374b
35 changed files with 811 additions and 83 deletions

View File

@ -12,9 +12,18 @@ RETURNING
*; *;
-- name: ExamPrepGetCatalogCourseByID :one -- name: ExamPrepGetCatalogCourseByID :one
SELECT * SELECT
FROM exam_prep.catalog_courses c.*,
WHERE id = $1; 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 -- name: ExamPrepListCatalogCourses :many
WITH catalog_course_counts AS ( WITH catalog_course_counts AS (
@ -38,6 +47,14 @@ SELECT
COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_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.created_at,
c.updated_at c.updated_at
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c

View File

@ -16,9 +16,15 @@ RETURNING
*; *;
-- name: ExamPrepGetUnitModuleLessonByID :one -- name: ExamPrepGetUnitModuleLessonByID :one
SELECT * SELECT
FROM exam_prep.unit_module_lessons l.*,
WHERE id = $1; 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 -- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
SELECT SELECT
@ -39,6 +45,11 @@ SELECT
l.thumbnail, l.thumbnail,
l.description, l.description,
l.sort_order, 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.created_at,
l.updated_at l.updated_at
FROM exam_prep.unit_module_lessons l FROM exam_prep.unit_module_lessons l

View File

@ -16,9 +16,16 @@ RETURNING
*; *;
-- name: ExamPrepGetUnitModuleByID :one -- name: ExamPrepGetUnitModuleByID :one
SELECT * SELECT
FROM exam_prep.unit_modules m.*,
WHERE id = $1; 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 -- name: ExamPrepListUnitModuleIDsByUnit :many
SELECT SELECT
@ -51,6 +58,7 @@ SELECT
m.sort_order, m.sort_order,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count, COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
m.created_at, m.created_at,
m.updated_at m.updated_at
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m

View File

@ -15,9 +15,17 @@ RETURNING
*; *;
-- name: ExamPrepGetUnitByID :one -- name: ExamPrepGetUnitByID :one
SELECT * SELECT
FROM exam_prep.units u.*,
WHERE id = $1; 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 -- name: ExamPrepListUnitIDsByCatalogCourse :many
SELECT SELECT
@ -52,6 +60,7 @@ SELECT
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count, COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_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 AS practices_count,
(COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice,
u.created_at, u.created_at,
u.updated_at u.updated_at
FROM exam_prep.units u FROM exam_prep.units u

View File

@ -15,9 +15,18 @@ RETURNING
*; *;
-- name: GetCourseByID :one -- 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 FROM courses
WHERE id = $1; c
WHERE c.id = $1;
-- name: ListCourseIDsByProgram :many -- name: ListCourseIDsByProgram :many
SELECT SELECT
@ -65,7 +74,14 @@ SELECT
WHERE WHERE
p.course_id = c.id p.course_id = c.id
AND p.module_id IS NULL 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 FROM
courses c courses c
WHERE WHERE

View File

@ -16,9 +16,16 @@ RETURNING
*; *;
-- name: GetLessonByID :one -- name: GetLessonByID :one
SELECT * SELECT
l.*,
EXISTS (
SELECT 1
FROM lms_practices p
WHERE p.lesson_id = l.id
) AS has_practice
FROM lessons FROM lessons
WHERE id = $1; l
WHERE l.id = $1;
-- name: ListLessonsByModuleID :many -- name: ListLessonsByModuleID :many
SELECT SELECT
@ -31,7 +38,12 @@ SELECT
l.description, l.description,
l.sort_order, l.sort_order,
l.created_at, 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 FROM
lessons l lessons l
WHERE WHERE

View File

@ -16,9 +16,17 @@ RETURNING
*; *;
-- name: GetModuleByID :one -- 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 FROM modules
WHERE id = $1; m
WHERE m.id = $1;
-- name: ListModuleIDsByCourse :many -- name: ListModuleIDsByCourse :many
SELECT SELECT
@ -41,7 +49,13 @@ SELECT
m.icon, m.icon,
m.sort_order, m.sort_order,
m.created_at, 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 FROM
modules m modules m
WHERE WHERE

View File

@ -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": { "domain.EmploymentType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -9638,6 +9692,9 @@ const docTemplate = `{
"difficultyLevel": { "difficultyLevel": {
"type": "string" "type": "string"
}, },
"dynamicPayload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -10927,6 +10984,9 @@ const docTemplate = `{
"difficulty_level": { "difficulty_level": {
"type": "string" "type": "string"
}, },
"dynamic_payload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -11046,6 +11106,12 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
}, },
"response_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
},
"status": { "status": {
"type": "string" "type": "string"
}, },
@ -11054,6 +11120,12 @@ const docTemplate = `{
"items": { "items": {
"type": "string" "type": "string"
} }
},
"stimulus_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
} }
} }
}, },
@ -11409,6 +11481,9 @@ const docTemplate = `{
"difficulty_level": { "difficulty_level": {
"type": "string" "type": "string"
}, },
"dynamic_payload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -11500,6 +11575,12 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
}, },
"response_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
},
"status": { "status": {
"type": "string" "type": "string"
}, },
@ -11508,6 +11589,12 @@ const docTemplate = `{
"items": { "items": {
"type": "string" "type": "string"
} }
},
"stimulus_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
} }
} }
}, },

View File

@ -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": { "domain.EmploymentType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -9630,6 +9684,9 @@
"difficultyLevel": { "difficultyLevel": {
"type": "string" "type": "string"
}, },
"dynamicPayload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -10919,6 +10976,9 @@
"difficulty_level": { "difficulty_level": {
"type": "string" "type": "string"
}, },
"dynamic_payload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -11038,6 +11098,12 @@
"type": "string" "type": "string"
} }
}, },
"response_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
},
"status": { "status": {
"type": "string" "type": "string"
}, },
@ -11046,6 +11112,12 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"stimulus_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
} }
} }
}, },
@ -11401,6 +11473,9 @@
"difficulty_level": { "difficulty_level": {
"type": "string" "type": "string"
}, },
"dynamic_payload": {
"$ref": "#/definitions/domain.DynamicQuestionPayload"
},
"explanation": { "explanation": {
"type": "string" "type": "string"
}, },
@ -11492,6 +11567,12 @@
"type": "string" "type": "string"
} }
}, },
"response_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
},
"status": { "status": {
"type": "string" "type": "string"
}, },
@ -11500,6 +11581,12 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"stimulus_schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.DynamicElementDefinition"
}
} }
} }
}, },

View File

@ -212,6 +212,42 @@ definitions:
- password - password
- team_role - team_role
type: object 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: domain.EmploymentType:
enum: enum:
- full_time - full_time
@ -371,6 +407,8 @@ definitions:
type: string type: string
difficultyLevel: difficultyLevel:
type: string type: string
dynamicPayload:
$ref: '#/definitions/domain.DynamicQuestionPayload'
explanation: explanation:
type: string type: string
id: id:
@ -1240,6 +1278,8 @@ definitions:
type: string type: string
difficulty_level: difficulty_level:
type: string type: string
dynamic_payload:
$ref: '#/definitions/domain.DynamicQuestionPayload'
explanation: explanation:
type: string type: string
image_url: image_url:
@ -1320,12 +1360,20 @@ definitions:
items: items:
type: string type: string
type: array type: array
response_schema:
items:
$ref: '#/definitions/domain.DynamicElementDefinition'
type: array
status: status:
type: string type: string
stimulus_component_kinds: stimulus_component_kinds:
items: items:
type: string type: string
type: array type: array
stimulus_schema:
items:
$ref: '#/definitions/domain.DynamicElementDefinition'
type: array
required: required:
- display_name - display_name
- key - key
@ -1567,6 +1615,8 @@ definitions:
type: string type: string
difficulty_level: difficulty_level:
type: string type: string
dynamic_payload:
$ref: '#/definitions/domain.DynamicQuestionPayload'
explanation: explanation:
type: string type: string
image_url: image_url:
@ -1627,12 +1677,20 @@ definitions:
items: items:
type: string type: string
type: array type: array
response_schema:
items:
$ref: '#/definitions/domain.DynamicElementDefinition'
type: array
status: status:
type: string type: string
stimulus_component_kinds: stimulus_component_kinds:
items: items:
type: string type: string
type: array type: array
stimulus_schema:
items:
$ref: '#/definitions/domain.DynamicElementDefinition'
type: array
type: object type: object
handlers.validateQuestionTypeDefinitionReq: handlers.validateQuestionTypeDefinitionReq:
properties: properties:

View File

@ -57,14 +57,34 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err
} }
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
SELECT id, name, description, thumbnail, sort_order, created_at, updated_at SELECT
FROM exam_prep.catalog_courses c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at,
WHERE id = $1 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) row := q.db.QueryRow(ctx, ExamPrepGetCatalogCourseByID, id)
var i ExamPrepCatalogCourse var i ExamPrepGetCatalogCourseByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.Name, &i.Name,
@ -73,6 +93,7 @@ func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (E
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -126,6 +147,14 @@ SELECT
COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_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.created_at,
c.updated_at c.updated_at
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c
@ -149,6 +178,7 @@ type ExamPrepListCatalogCoursesRow struct {
UnitsCount int64 `json:"units_count"` UnitsCount int64 `json:"units_count"`
ModulesCount int64 `json:"modules_count"` ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"` LessonsCount int64 `json:"lessons_count"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
@ -172,6 +202,7 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi
&i.UnitsCount, &i.UnitsCount,
&i.ModulesCount, &i.ModulesCount,
&i.LessonsCount, &i.LessonsCount,
&i.HasPractice,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {

View File

@ -71,14 +71,33 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64)
} }
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
SELECT id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at SELECT
FROM exam_prep.unit_module_lessons l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at,
WHERE id = $1 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) row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleLessonByID, id)
var i ExamPrepUnitModuleLesson var i ExamPrepGetUnitModuleLessonByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UnitModuleID, &i.UnitModuleID,
@ -89,6 +108,7 @@ func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64)
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -133,6 +153,11 @@ SELECT
l.thumbnail, l.thumbnail,
l.description, l.description,
l.sort_order, 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.created_at,
l.updated_at l.updated_at
FROM exam_prep.unit_module_lessons l FROM exam_prep.unit_module_lessons l
@ -160,6 +185,7 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
@ -182,6 +208,7 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex
&i.Thumbnail, &i.Thumbnail,
&i.Description, &i.Description,
&i.SortOrder, &i.SortOrder,
&i.HasPractice,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {

View File

@ -71,14 +71,34 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error
} }
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
SELECT id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at SELECT
FROM exam_prep.unit_modules m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at,
WHERE id = $1 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) row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleByID, id)
var i ExamPrepUnitModule var i ExamPrepGetUnitModuleByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UnitID, &i.UnitID,
@ -89,6 +109,7 @@ func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (Exam
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -145,6 +166,7 @@ SELECT
m.sort_order, m.sort_order,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count, COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
m.created_at, m.created_at,
m.updated_at m.updated_at
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
@ -175,6 +197,7 @@ type ExamPrepListUnitModulesByUnitRow struct {
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
LessonsCount int64 `json:"lessons_count"` LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"` PracticesCount int64 `json:"practices_count"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
@ -199,6 +222,7 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
&i.SortOrder, &i.SortOrder,
&i.LessonsCount, &i.LessonsCount,
&i.PracticesCount, &i.PracticesCount,
&i.HasPractice,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {

View File

@ -67,14 +67,34 @@ func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error {
} }
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
SELECT id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at SELECT
FROM exam_prep.units u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at,
WHERE id = $1 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) row := q.db.QueryRow(ctx, ExamPrepGetUnitByID, id)
var i ExamPrepUnit var i ExamPrepGetUnitByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.CatalogCourseID, &i.CatalogCourseID,
@ -84,6 +104,7 @@ func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepUn
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -142,6 +163,7 @@ SELECT
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count, COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_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 AS practices_count,
(COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice,
u.created_at, u.created_at,
u.updated_at u.updated_at
FROM exam_prep.units u FROM exam_prep.units u
@ -172,6 +194,7 @@ type ExamPrepListUnitsByCatalogCourseRow struct {
ModulesCount int64 `json:"modules_count"` ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"` LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"` PracticesCount int64 `json:"practices_count"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
@ -196,6 +219,7 @@ func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg Exam
&i.ModulesCount, &i.ModulesCount,
&i.LessonsCount, &i.LessonsCount,
&i.PracticesCount, &i.PracticesCount,
&i.HasPractice,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {

View File

@ -67,14 +67,35 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
} }
const GetCourseByID = `-- name: GetCourseByID :one 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 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) row := q.db.QueryRow(ctx, GetCourseByID, id)
var i Course var i GetCourseByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProgramID, &i.ProgramID,
@ -84,6 +105,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -155,7 +177,14 @@ SELECT
WHERE WHERE
p.course_id = c.id p.course_id = c.id
AND p.module_id IS NULL 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 FROM
courses c courses c
WHERE WHERE
@ -185,6 +214,7 @@ type ListCoursesByProgramIDRow struct {
ModuleCount int64 `json:"module_count"` ModuleCount int64 `json:"module_count"`
LessonCount int64 `json:"lesson_count"` LessonCount int64 `json:"lesson_count"`
PracticeCount int64 `json:"practice_count"` PracticeCount int64 `json:"practice_count"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) { 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.ModuleCount,
&i.LessonCount, &i.LessonCount,
&i.PracticeCount, &i.PracticeCount,
&i.HasPractice,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -71,14 +71,34 @@ func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
} }
const GetLessonByID = `-- name: GetLessonByID :one 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 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) row := q.db.QueryRow(ctx, GetLessonByID, id)
var i Lesson var i GetLessonByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ModuleID, &i.ModuleID,
@ -89,6 +109,7 @@ func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) {
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -104,7 +125,12 @@ SELECT
l.description, l.description,
l.sort_order, l.sort_order,
l.created_at, 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 FROM
lessons l lessons l
WHERE WHERE
@ -133,6 +159,7 @@ type ListLessonsByModuleIDRow struct {
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) { 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.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -71,14 +71,35 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
} }
const GetModuleByID = `-- name: GetModuleByID :one 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 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) row := q.db.QueryRow(ctx, GetModuleByID, id)
var i Module var i GetModuleByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProgramID, &i.ProgramID,
@ -89,6 +110,7 @@ func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.HasPractice,
) )
return i, err return i, err
} }
@ -135,7 +157,13 @@ SELECT
m.icon, m.icon,
m.sort_order, m.sort_order,
m.created_at, 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 FROM
modules m modules m
WHERE WHERE
@ -166,6 +194,7 @@ type ListModulesByProgramAndCourseRow struct {
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) { 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.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -28,6 +28,7 @@ type Course struct {
ModuleCount int `json:"module_count"` ModuleCount int `json:"module_count"`
LessonCount int `json:"lesson_count"` LessonCount int `json:"lesson_count"`
PracticeCount int `json:"practice_count"` PracticeCount int `json:"practice_count"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"`
} }

View File

@ -12,6 +12,7 @@ type ExamPrepCatalogCourse struct {
UnitsCount *int64 `json:"units_count,omitempty"` UnitsCount *int64 `json:"units_count,omitempty"`
ModulesCount *int64 `json:"modules_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@ -11,6 +11,7 @@ type ExamPrepLesson struct {
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@ -13,6 +13,7 @@ type ExamPrepModule struct {
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@ -13,6 +13,7 @@ type ExamPrepUnit struct {
ModulesCount *int64 `json:"modules_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@ -11,6 +11,7 @@ type Lesson struct {
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"`

View File

@ -11,6 +11,7 @@ type Module struct {
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"`

View File

@ -47,7 +47,17 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
} }
return domain.ExamPrepCatalogCourse{}, err 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) { 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.UnitsCount = &r.UnitsCount
item.ModulesCount = &r.ModulesCount item.ModulesCount = &r.ModulesCount
item.LessonsCount = &r.LessonsCount item.LessonsCount = &r.LessonsCount
item.HasPractice = r.HasPractice
out = append(out, item) out = append(out, item)
} }
return out, total, nil return out, total, nil

View File

@ -51,7 +51,19 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
} }
return domain.ExamPrepLesson{}, err 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) { 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 { if i == 0 {
total = r.TotalCount total = r.TotalCount
} }
out = append(out, examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
ID: r.ID, ID: r.ID,
UnitModuleID: r.UnitModuleID, UnitModuleID: r.UnitModuleID,
Title: r.Title, Title: r.Title,
@ -82,7 +94,9 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
})) })
item.HasPractice = r.HasPractice
out = append(out, item)
} }
return out, total, nil return out, total, nil
} }

View File

@ -51,7 +51,19 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
} }
return domain.ExamPrepModule{}, err 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) { 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.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount item.PracticesCount = &r.PracticesCount
item.HasPractice = r.HasPractice
out = append(out, item) out = append(out, item)
} }
return out, total, nil return out, total, nil

View File

@ -49,7 +49,18 @@ func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamP
} }
return domain.ExamPrepUnit{}, err 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) { 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.ModulesCount = &r.ModulesCount
item.LessonsCount = &r.LessonsCount item.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount item.PracticesCount = &r.PracticesCount
item.HasPractice = r.HasPractice
out = append(out, item) out = append(out, item)
} }
return out, total, nil return out, total, nil

View File

@ -53,7 +53,18 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err
} }
return 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) { 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.ModuleCount = int(r.ModuleCount)
co.LessonCount = int(r.LessonCount) co.LessonCount = int(r.LessonCount)
co.PracticeCount = int(r.PracticeCount) co.PracticeCount = int(r.PracticeCount)
co.HasPractice = r.HasPractice
out = append(out, co) out = append(out, co)
} }
return out, total, nil return out, total, nil

View File

@ -51,7 +51,19 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err
} }
return 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) { 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 { if i == 0 {
total = r.TotalCount total = r.TotalCount
} }
out = append(out, lessonToDomain(dbgen.Lesson{ lesson := lessonToDomain(dbgen.Lesson{
ID: r.ID, ID: r.ID,
ModuleID: r.ModuleID, ModuleID: r.ModuleID,
Title: r.Title, Title: r.Title,
@ -82,7 +94,9 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
})) })
lesson.HasPractice = r.HasPractice
out = append(out, lesson)
} }
return out, total, nil return out, total, nil
} }

View File

@ -55,7 +55,19 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err
} }
return 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) { 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 { if i == 0 {
total = r.TotalCount total = r.TotalCount
} }
out = append(out, moduleToDomain(dbgen.Module{ mod := moduleToDomain(dbgen.Module{
ID: r.ID, ID: r.ID,
ProgramID: r.ProgramID, ProgramID: r.ProgramID,
CourseID: r.CourseID, CourseID: r.CourseID,
@ -87,7 +99,9 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
})) })
mod.HasPractice = r.HasPractice
out = append(out, mod)
} }
return out, total, nil return out, total, nil
} }

View File

@ -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 { func (s *Service) SendEmailWithAttachments(ctx context.Context, receiverEmail, message string, messageHTML string, subject string, attachments []*resend.Attachment) error {
apiKey := s.config.ResendApiKey apiKey := s.config.ResendApiKey
client := resend.NewClient(apiKey) client := resend.NewClient(apiKey)
formattedSenderEmail := "Y <" + s.config.ResendSenderEmail + ">" formattedSenderEmail := "Yimaru - Academy <" + s.config.ResendSenderEmail + ">"
params := &resend.SendEmailRequest{ params := &resend.SendEmailRequest{
From: formattedSenderEmail, From: formattedSenderEmail,
To: []string{receiverEmail}, To: []string{receiverEmail},

View File

@ -2,6 +2,9 @@ package handlers
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context"
"encoding/json"
"fmt"
"strconv" "strconv"
"strings" "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{ return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Question type definition created", Message: "Question type definition created",
Data: def, 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{ return c.JSON(domain.Response{
Message: "Question type definition updated", Message: "Question type definition updated",
Data: fiber.Map{"id": id}, 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{ return c.JSON(domain.Response{
Message: "Question type definition deleted", Message: "Question type definition deleted",
Data: fiber.Map{"id": id}, Data: fiber.Map{"id": id},

View File

@ -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{ return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Question added to set successfully", Message: "Question added to set successfully",
Data: map[string]interface{}{ 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{ return c.JSON(domain.Response{
Message: "Question removed from set successfully", 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{ return c.JSON(domain.Response{
Message: "Question order updated successfully", 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{ return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Persona added to question set successfully", 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{ return c.JSON(domain.Response{
Message: "Persona removed from question set successfully", Message: "Persona removed from question set successfully",
}) })

View File

@ -310,6 +310,18 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/notifications", a.authMiddleware, a.RequirePermission("notifications.delete_mine"), h.DeleteUserNotifications) 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.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 // Issues
groupV1.Post("/issues", a.authMiddleware, a.RequirePermission("issues.create"), h.CreateIssue) 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/register", a.authMiddleware, a.RequirePermission("devices.register"), h.RegisterDeviceToken)
groupV1.Post("/devices/unregister", a.authMiddleware, a.RequirePermission("devices.unregister"), h.UnregisterDeviceToken) 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 // Settings
groupV1.Get("/settings", a.authMiddleware, a.RequirePermission("settings.list"), h.GetGlobalSettingList) groupV1.Get("/settings", a.authMiddleware, a.RequirePermission("settings.list"), h.GetGlobalSettingList)
groupV1.Get("/settings/:key", a.authMiddleware, a.RequirePermission("settings.get"), h.GetGlobalSettingByKey) groupV1.Get("/settings/:key", a.authMiddleware, a.RequirePermission("settings.get"), h.GetGlobalSettingByKey)