free/premium feature

This commit is contained in:
Yared Yemane 2026-06-10 04:23:47 -07:00
parent 26cf7d2908
commit c9a4bc1306
46 changed files with 957 additions and 145 deletions

View File

@ -0,0 +1,23 @@
ALTER TABLE exam_prep.unit_module_lessons DROP CONSTRAINT IF EXISTS chk_exam_prep_unit_module_lessons_access_tier;
ALTER TABLE exam_prep.unit_module_lessons DROP COLUMN IF EXISTS access_tier;
ALTER TABLE exam_prep.unit_modules DROP CONSTRAINT IF EXISTS chk_exam_prep_unit_modules_access_tier;
ALTER TABLE exam_prep.unit_modules DROP COLUMN IF EXISTS access_tier;
ALTER TABLE exam_prep.units DROP CONSTRAINT IF EXISTS chk_exam_prep_units_access_tier;
ALTER TABLE exam_prep.units DROP COLUMN IF EXISTS access_tier;
ALTER TABLE exam_prep.catalog_courses DROP CONSTRAINT IF EXISTS chk_exam_prep_catalog_courses_access_tier;
ALTER TABLE exam_prep.catalog_courses DROP COLUMN IF EXISTS access_tier;
ALTER TABLE lessons DROP CONSTRAINT IF EXISTS chk_lessons_access_tier;
ALTER TABLE lessons DROP COLUMN IF EXISTS access_tier;
ALTER TABLE modules DROP CONSTRAINT IF EXISTS chk_modules_access_tier;
ALTER TABLE modules DROP COLUMN IF EXISTS access_tier;
ALTER TABLE courses DROP CONSTRAINT IF EXISTS chk_courses_access_tier;
ALTER TABLE courses DROP COLUMN IF EXISTS access_tier;
ALTER TABLE programs DROP CONSTRAINT IF EXISTS chk_programs_access_tier;
ALTER TABLE programs DROP COLUMN IF EXISTS access_tier;

View File

@ -0,0 +1,34 @@
-- FREE content is accessible without a subscription; PREMIUM requires one.
-- Effective tier cascades at read time: any PREMIUM ancestor makes descendants PREMIUM.
ALTER TABLE programs
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_programs_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE courses
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_courses_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE modules
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_modules_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE lessons
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_lessons_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE exam_prep.catalog_courses
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_exam_prep_catalog_courses_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE exam_prep.units
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_exam_prep_units_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE exam_prep.unit_modules
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_exam_prep_unit_modules_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));
ALTER TABLE exam_prep.unit_module_lessons
ADD COLUMN access_tier VARCHAR(16) NOT NULL DEFAULT 'PREMIUM'
CONSTRAINT chk_exam_prep_unit_module_lessons_access_tier CHECK (access_tier IN ('FREE', 'PREMIUM'));

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateCatalogCourse :one
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status)
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -9,7 +9,8 @@ SELECT
SELECT
max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
$5,
$6
RETURNING
*;
@ -55,6 +56,7 @@ SELECT
c.thumbnail,
c.sort_order,
c.publish_status,
c.access_tier,
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,
@ -96,6 +98,7 @@ SET
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = coalesce(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnitModuleLesson :one
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status)
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -12,7 +12,8 @@ SELECT
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1), 0) + 1,
$6
$6,
$7
RETURNING
*;
@ -59,6 +60,7 @@ SELECT
l.description,
l.sort_order,
l.publish_status,
l.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -89,6 +91,7 @@ SET
description = coalesce(sqlc.narg('description')::text, description),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = coalesce(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnitModule :one
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status)
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -12,7 +12,8 @@ SELECT
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1), 0) + 1,
$6
$6,
$7
RETURNING
*;
@ -73,6 +74,7 @@ SELECT
m.icon,
m.sort_order,
m.publish_status,
m.access_tier,
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,
@ -101,6 +103,7 @@ SET
icon = coalesce(sqlc.narg('icon')::text, icon),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = coalesce(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnit :one
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status)
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status, access_tier)
SELECT
sqlc.arg('catalog_course_id'),
sqlc.arg('name'),
@ -12,7 +12,8 @@ SELECT
FROM exam_prep.units u
WHERE
u.catalog_course_id = sqlc.arg('catalog_course_id')), 0) + 1),
sqlc.arg('publish_status')
sqlc.arg('publish_status'),
sqlc.arg('access_tier')
RETURNING
*;
@ -77,6 +78,7 @@ SELECT
u.thumbnail,
u.sort_order,
u.publish_status,
u.access_tier,
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,
@ -105,6 +107,7 @@ SET
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = coalesce(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: CreateCourse :one
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status)
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status, access_tier)
SELECT
sqlc.arg('program_id'),
sqlc.arg('name'),
@ -12,7 +12,8 @@ SELECT
FROM courses c
WHERE
c.program_id = sqlc.arg('program_id')), 0) + 1),
sqlc.arg('publish_status')
sqlc.arg('publish_status'),
sqlc.arg('access_tier')
RETURNING
*;
@ -63,6 +64,7 @@ SELECT
c.thumbnail,
c.sort_order,
c.publish_status,
c.access_tier,
c.created_at,
c.updated_at,
(
@ -123,6 +125,7 @@ SET
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = COALESCE(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -1,5 +1,5 @@
-- name: CreateLesson :one
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status)
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status, access_tier)
SELECT
sqlc.arg('module_id'),
sqlc.arg('title'),
@ -13,7 +13,8 @@ SELECT
FROM lessons l
WHERE
l.module_id = sqlc.arg('module_id')), 0) + 1),
sqlc.arg('publish_status')
sqlc.arg('publish_status'),
sqlc.arg('access_tier')
RETURNING
*;
@ -51,6 +52,7 @@ SELECT
l.description,
l.sort_order,
l.publish_status,
l.access_tier,
l.created_at,
l.updated_at,
EXISTS (
@ -82,6 +84,7 @@ SET
description = COALESCE(sqlc.narg('description')::text, description),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = COALESCE(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -1,5 +1,5 @@
-- name: CreateModule :one
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status)
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status, access_tier)
SELECT
sqlc.arg('program_id'),
sqlc.arg('course_id'),
@ -13,7 +13,8 @@ SELECT
FROM modules m
WHERE
m.course_id = sqlc.arg('course_id')), 0) + 1),
sqlc.arg('publish_status')
sqlc.arg('publish_status'),
sqlc.arg('access_tier')
RETURNING
*;
@ -64,6 +65,7 @@ SELECT
m.icon,
m.sort_order,
m.publish_status,
m.access_tier,
m.created_at,
m.updated_at,
EXISTS (
@ -96,6 +98,7 @@ SET
icon = COALESCE(sqlc.narg('icon')::text, icon),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = COALESCE(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -1,5 +1,5 @@
-- name: CreateProgram :one
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status)
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status, access_tier)
SELECT
sqlc.arg('name'),
sqlc.arg('description'),
@ -9,7 +9,8 @@ SELECT
SELECT
max(p.sort_order)
FROM programs AS p), 0) + 1),
sqlc.arg('publish_status')
sqlc.arg('publish_status'),
sqlc.arg('access_tier')
RETURNING
*;
@ -36,6 +37,7 @@ SELECT
p.thumbnail,
p.sort_order,
p.publish_status,
p.access_tier,
p.created_at,
p.updated_at
FROM programs p
@ -59,6 +61,7 @@ SET
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
access_tier = COALESCE(sqlc.narg('access_tier')::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateCatalogCourse = `-- name: ExamPrepCreateCatalogCourse :one
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status)
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -22,9 +22,10 @@ SELECT
SELECT
max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
$5,
$6
RETURNING
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status, access_tier
`
type ExamPrepCreateCatalogCourseParams struct {
@ -33,6 +34,7 @@ type ExamPrepCreateCatalogCourseParams struct {
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepCreateCatalogCourseParams) (ExamPrepCatalogCourse, error) {
@ -42,6 +44,7 @@ func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepC
arg.Category,
arg.Thumbnail,
arg.PublishStatus,
arg.AccessTier,
)
var i ExamPrepCatalogCourse
err := row.Scan(
@ -54,6 +57,7 @@ func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepC
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -70,7 +74,7 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
SELECT
c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at, c.category, c.publish_status,
c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at, c.category, c.publish_status, c.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -97,6 +101,7 @@ type ExamPrepGetCatalogCourseByIDRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -113,6 +118,7 @@ func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (E
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -169,6 +175,7 @@ SELECT
c.thumbnail,
c.sort_order,
c.publish_status,
c.access_tier,
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,
@ -211,6 +218,7 @@ type ExamPrepListCatalogCoursesRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
UnitsCount int64 `json:"units_count"`
ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"`
@ -237,6 +245,7 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.UnitsCount,
&i.ModulesCount,
&i.LessonsCount,
@ -263,10 +272,11 @@ SET
thumbnail = coalesce($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
access_tier = coalesce($7::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
WHERE id = $8
RETURNING
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status, access_tier
`
type ExamPrepUpdateCatalogCourseParams struct {
@ -276,6 +286,7 @@ type ExamPrepUpdateCatalogCourseParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -287,6 +298,7 @@ func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepU
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i ExamPrepCatalogCourse
@ -300,6 +312,7 @@ func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepU
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnitModuleLesson = `-- name: ExamPrepCreateUnitModuleLesson :one
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status)
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -25,9 +25,10 @@ SELECT
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1), 0) + 1,
$6
$6,
$7
RETURNING
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepCreateUnitModuleLessonParams struct {
@ -37,6 +38,7 @@ type ExamPrepCreateUnitModuleLessonParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPrepCreateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) {
@ -47,6 +49,7 @@ func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPr
arg.Thumbnail,
arg.Description,
arg.PublishStatus,
arg.AccessTier,
)
var i ExamPrepUnitModuleLesson
err := row.Scan(
@ -60,6 +63,7 @@ func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -76,7 +80,7 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64)
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
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, l.publish_status,
l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at, l.publish_status, l.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -98,6 +102,7 @@ type ExamPrepGetUnitModuleLessonByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -115,6 +120,7 @@ func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64)
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -193,6 +199,7 @@ SELECT
l.description,
l.sort_order,
l.publish_status,
l.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -232,6 +239,7 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
@ -261,6 +269,7 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex
&i.Description,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
&i.CreatedAt,
&i.UpdatedAt,
@ -284,10 +293,11 @@ SET
description = coalesce($4::text, description),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
access_tier = coalesce($7::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
WHERE id = $8
RETURNING
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepUpdateUnitModuleLessonParams struct {
@ -297,6 +307,7 @@ type ExamPrepUpdateUnitModuleLessonParams struct {
Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -308,6 +319,7 @@ func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPr
arg.Description,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i ExamPrepUnitModuleLesson
@ -322,6 +334,7 @@ func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnitModule = `-- name: ExamPrepCreateUnitModule :one
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status)
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -25,9 +25,10 @@ SELECT
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1), 0) + 1,
$6
$6,
$7
RETURNING
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepCreateUnitModuleParams struct {
@ -37,6 +38,7 @@ type ExamPrepCreateUnitModuleParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCreateUnitModuleParams) (ExamPrepUnitModule, error) {
@ -47,6 +49,7 @@ func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCrea
arg.Thumbnail,
arg.Icon,
arg.PublishStatus,
arg.AccessTier,
)
var i ExamPrepUnitModule
err := row.Scan(
@ -60,6 +63,7 @@ func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCrea
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -76,7 +80,7 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
SELECT
m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at, m.publish_status,
m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at, m.publish_status, m.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -100,6 +104,7 @@ type ExamPrepGetUnitModuleByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -117,6 +122,7 @@ func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (Exam
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -207,6 +213,7 @@ SELECT
m.icon,
m.sort_order,
m.publish_status,
m.access_tier,
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,
@ -244,6 +251,7 @@ type ExamPrepListUnitModulesByUnitRow struct {
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"`
HasPractice bool `json:"has_practice"`
@ -275,6 +283,7 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
&i.Icon,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.LessonsCount,
&i.PracticesCount,
&i.HasPractice,
@ -300,10 +309,11 @@ SET
icon = coalesce($4::text, icon),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
access_tier = coalesce($7::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
WHERE id = $8
RETURNING
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepUpdateUnitModuleParams struct {
@ -313,6 +323,7 @@ type ExamPrepUpdateUnitModuleParams struct {
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -324,6 +335,7 @@ func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpda
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i ExamPrepUnitModule
@ -338,6 +350,7 @@ func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpda
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnit = `-- name: ExamPrepCreateUnit :one
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status)
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -25,9 +25,10 @@ SELECT
FROM exam_prep.units u
WHERE
u.catalog_course_id = $1), 0) + 1),
$6
$6,
$7
RETURNING
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepCreateUnitParams struct {
@ -37,6 +38,7 @@ type ExamPrepCreateUnitParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnitParams) (ExamPrepUnit, error) {
@ -47,6 +49,7 @@ func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnit
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
)
var i ExamPrepUnit
err := row.Scan(
@ -59,6 +62,7 @@ func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnit
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -75,7 +79,7 @@ func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error {
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
SELECT
u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at, u.publish_status,
u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at, u.publish_status, u.access_tier,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -100,6 +104,7 @@ type ExamPrepGetUnitByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -116,6 +121,7 @@ func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepGe
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -208,6 +214,7 @@ SELECT
u.thumbnail,
u.sort_order,
u.publish_status,
u.access_tier,
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,
@ -245,6 +252,7 @@ type ExamPrepListUnitsByCatalogCourseRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"`
@ -276,6 +284,7 @@ func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg Exam
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.ModulesCount,
&i.LessonsCount,
&i.PracticesCount,
@ -301,10 +310,11 @@ SET
thumbnail = coalesce($3::text, thumbnail),
sort_order = coalesce($4::int, sort_order),
publish_status = coalesce($5::varchar, publish_status),
access_tier = coalesce($6::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE id = $6
WHERE id = $7
RETURNING
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status, access_tier
`
type ExamPrepUpdateUnitParams struct {
@ -313,6 +323,7 @@ type ExamPrepUpdateUnitParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -323,6 +334,7 @@ func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnit
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i ExamPrepUnit
@ -336,6 +348,7 @@ func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnit
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const CreateCourse = `-- name: CreateCourse :one
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status)
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -25,9 +25,10 @@ SELECT
FROM courses c
WHERE
c.program_id = $1), 0) + 1),
$6
$6,
$7
RETURNING
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status, access_tier
`
type CreateCourseParams struct {
@ -37,6 +38,7 @@ type CreateCourseParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
@ -47,6 +49,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
)
var i Course
err := row.Scan(
@ -59,6 +62,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -75,7 +79,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
const GetCourseByID = `-- name: GetCourseByID :one
SELECT
c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order, c.publish_status,
c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order, c.publish_status, c.access_tier,
EXISTS (
SELECT 1
FROM lms_practices p
@ -99,6 +103,7 @@ type GetCourseByIDRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -115,6 +120,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -161,6 +167,7 @@ SELECT
c.thumbnail,
c.sort_order,
c.publish_status,
c.access_tier,
c.created_at,
c.updated_at,
(
@ -230,6 +237,7 @@ type ListCoursesByProgramIDRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ModuleCount int64 `json:"module_count"`
@ -261,6 +269,7 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModuleCount,
@ -319,11 +328,12 @@ SET
thumbnail = COALESCE($3::text, thumbnail),
sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
access_tier = COALESCE($6::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $6
id = $7
RETURNING
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status, access_tier
`
type UpdateCourseParams struct {
@ -332,6 +342,7 @@ type UpdateCourseParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -342,6 +353,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Cou
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i Course
@ -355,6 +367,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Cou
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const CreateLesson = `-- name: CreateLesson :one
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status)
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -26,9 +26,10 @@ SELECT
FROM lessons l
WHERE
l.module_id = $1), 0) + 1),
$7
$7,
$8
RETURNING
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status, access_tier
`
type CreateLessonParams struct {
@ -39,6 +40,7 @@ type CreateLessonParams struct {
Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
@ -50,6 +52,7 @@ func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Les
arg.Description,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
)
var i Lesson
err := row.Scan(
@ -63,6 +66,7 @@ func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Les
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -79,7 +83,7 @@ func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
const GetLessonByID = `-- name: GetLessonByID :one
SELECT
l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order, l.publish_status,
l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order, l.publish_status, l.access_tier,
EXISTS (
SELECT 1
FROM lms_practices p
@ -102,6 +106,7 @@ type GetLessonByIDRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -119,6 +124,7 @@ func (q *Queries) GetLessonByID(ctx context.Context, id int64) (GetLessonByIDRow
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -166,6 +172,7 @@ SELECT
l.description,
l.sort_order,
l.publish_status,
l.access_tier,
l.created_at,
l.updated_at,
EXISTS (
@ -206,6 +213,7 @@ type ListLessonsByModuleIDRow struct {
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
@ -235,6 +243,7 @@ func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByMo
&i.Description,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.CreatedAt,
&i.UpdatedAt,
&i.HasPractice,
@ -258,11 +267,12 @@ SET
description = COALESCE($4::text, description),
sort_order = coalesce($5::int, sort_order),
publish_status = COALESCE($6::varchar, publish_status),
access_tier = COALESCE($7::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $7
id = $8
RETURNING
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status, access_tier
`
type UpdateLessonParams struct {
@ -272,6 +282,7 @@ type UpdateLessonParams struct {
Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -283,6 +294,7 @@ func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Les
arg.Description,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i Lesson
@ -297,6 +309,7 @@ func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Les
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const CreateModule = `-- name: CreateModule :one
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status)
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -26,9 +26,10 @@ SELECT
FROM modules m
WHERE
m.course_id = $2), 0) + 1),
$7
$7,
$8
RETURNING
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status, access_tier
`
type CreateModuleParams struct {
@ -39,6 +40,7 @@ type CreateModuleParams struct {
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
@ -50,6 +52,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
)
var i Module
err := row.Scan(
@ -63,6 +66,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -79,7 +83,7 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
const GetModuleByID = `-- name: GetModuleByID :one
SELECT
m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order, m.publish_status,
m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order, m.publish_status, m.access_tier,
EXISTS (
SELECT 1
FROM lms_practices p
@ -103,6 +107,7 @@ type GetModuleByIDRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
HasPractice bool `json:"has_practice"`
}
@ -120,6 +125,7 @@ func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.HasPractice,
)
return i, err
@ -167,6 +173,7 @@ SELECT
m.icon,
m.sort_order,
m.publish_status,
m.access_tier,
m.created_at,
m.updated_at,
EXISTS (
@ -210,6 +217,7 @@ type ListModulesByProgramAndCourseRow struct {
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
@ -240,6 +248,7 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
&i.Icon,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.CreatedAt,
&i.UpdatedAt,
&i.HasPractice,
@ -295,11 +304,12 @@ SET
icon = COALESCE($3::text, icon),
sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
access_tier = COALESCE($6::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $6
id = $7
RETURNING
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status, access_tier
`
type UpdateModuleParams struct {
@ -308,6 +318,7 @@ type UpdateModuleParams struct {
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -318,6 +329,7 @@ func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Mod
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i Module
@ -332,6 +344,7 @@ func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Mod
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -667,7 +667,7 @@ func (q *Queries) GetPracticeScopeByQuestionSetID(ctx context.Context, questionS
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
SELECT
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order, c2.publish_status
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order, c2.publish_status, c2.access_tier
FROM
courses AS c1
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
@ -700,13 +700,14 @@ func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Cou
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
const GetPreviousLessonInModule = `-- name: GetPreviousLessonInModule :one
SELECT
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order, l2.publish_status
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order, l2.publish_status, l2.access_tier
FROM
lessons AS l1
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
@ -741,13 +742,14 @@ func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Less
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
const GetPreviousModuleInCourse = `-- name: GetPreviousModuleInCourse :one
SELECT
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order, m2.publish_status
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order, m2.publish_status, m2.access_tier
FROM
modules AS m1
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
@ -781,13 +783,14 @@ func (q *Queries) GetPreviousModuleInCourse(ctx context.Context, id int64) (Modu
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
const GetPreviousProgram = `-- name: GetPreviousProgram :one
SELECT
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order, p2.category, p2.publish_status
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order, p2.category, p2.publish_status, p2.access_tier
FROM
programs AS p1
INNER JOIN programs AS p2 ON p2.category = p1.category
@ -821,6 +824,7 @@ func (q *Queries) GetPreviousProgram(ctx context.Context, id int64) (Program, er
&i.SortOrder,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -32,6 +32,7 @@ type Course struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type Device struct {
@ -68,6 +69,7 @@ type ExamPrepCatalogCourse struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type ExamPrepLessonPractice struct {
@ -94,6 +96,7 @@ type ExamPrepUnit struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type ExamPrepUnitModule struct {
@ -107,6 +110,7 @@ type ExamPrepUnitModule struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type ExamPrepUnitModuleLesson struct {
@ -120,6 +124,7 @@ type ExamPrepUnitModuleLesson struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type Faq struct {
@ -162,6 +167,7 @@ type Lesson struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type LevelToSubCourse struct {
@ -245,6 +251,7 @@ type Module struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type ModuleToSubCourse struct {
@ -318,6 +325,7 @@ type Program struct {
SortOrder int32 `json:"sort_order"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
type Question struct {

View File

@ -12,7 +12,7 @@ import (
)
const CreateProgram = `-- name: CreateProgram :one
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status)
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status, access_tier)
SELECT
$1,
$2,
@ -22,9 +22,10 @@ SELECT
SELECT
max(p.sort_order)
FROM programs AS p), 0) + 1),
$6
$6,
$7
RETURNING
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status, access_tier
`
type CreateProgramParams struct {
@ -34,6 +35,7 @@ type CreateProgramParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
}
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) {
@ -44,6 +46,7 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
)
var i Program
err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
&i.SortOrder,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -71,7 +75,7 @@ func (q *Queries) DeleteProgram(ctx context.Context, id int64) error {
}
const GetProgramByID = `-- name: GetProgramByID :one
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status, access_tier
FROM programs
WHERE id = $1
`
@ -89,6 +93,7 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error)
&i.SortOrder,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}
@ -132,6 +137,7 @@ SELECT
p.thumbnail,
p.sort_order,
p.publish_status,
p.access_tier,
p.created_at,
p.updated_at
FROM programs p
@ -163,6 +169,7 @@ type ListProgramsRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
AccessTier string `json:"access_tier"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
@ -190,6 +197,7 @@ func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]L
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.AccessTier,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -212,11 +220,12 @@ SET
thumbnail = COALESCE($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order),
publish_status = COALESCE($6::varchar, publish_status),
access_tier = COALESCE($7::varchar, access_tier),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $7
id = $8
RETURNING
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status, access_tier
`
type UpdateProgramParams struct {
@ -226,6 +235,7 @@ type UpdateProgramParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
AccessTier pgtype.Text `json:"access_tier"`
ID int64 `json:"id"`
}
@ -237,6 +247,7 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.AccessTier,
arg.ID,
)
var i Program
@ -250,6 +261,7 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P
&i.SortOrder,
&i.Category,
&i.PublishStatus,
&i.AccessTier,
)
return i, err
}

View File

@ -0,0 +1,45 @@
package domain
import "strings"
// ContentAccessTier controls whether learners need an active subscription to consume content.
// Effective tier cascades at read time: any PREMIUM ancestor makes descendants PREMIUM.
type ContentAccessTier string
const (
ContentAccessFree ContentAccessTier = "FREE"
ContentAccessPremium ContentAccessTier = "PREMIUM"
)
// ContentAccessTierFromDB normalizes persisted values.
func ContentAccessTierFromDB(raw string) ContentAccessTier {
switch strings.TrimSpace(strings.ToUpper(raw)) {
case string(ContentAccessFree):
return ContentAccessFree
default:
return ContentAccessPremium
}
}
// ContentAccessTierFromCreateInput resolves create body: omit → premium; explicit value validated separately.
func ContentAccessTierFromCreateInput(raw *string) ContentAccessTier {
if raw == nil || strings.TrimSpace(*raw) == "" {
return ContentAccessPremium
}
return ContentAccessTierFromDB(*raw)
}
// EffectiveContentAccessTier returns PREMIUM when any tier in the chain is PREMIUM.
func EffectiveContentAccessTier(tiers ...ContentAccessTier) ContentAccessTier {
for _, tier := range tiers {
if tier == ContentAccessPremium {
return ContentAccessPremium
}
}
return ContentAccessFree
}
// RequiresSubscription is true when the effective tier needs an active plan.
func (t ContentAccessTier) RequiresSubscription() bool {
return t == ContentAccessPremium
}

View File

@ -0,0 +1,15 @@
package domain
import "testing"
func TestEffectiveContentAccessTier(t *testing.T) {
if got := EffectiveContentAccessTier(ContentAccessFree, ContentAccessFree); got != ContentAccessFree {
t.Fatalf("expected FREE, got %s", got)
}
if got := EffectiveContentAccessTier(ContentAccessFree, ContentAccessPremium); got != ContentAccessPremium {
t.Fatalf("expected PREMIUM, got %s", got)
}
if got := EffectiveContentAccessTier(ContentAccessPremium, ContentAccessFree); got != ContentAccessPremium {
t.Fatalf("expected cascaded PREMIUM, got %s", got)
}
}

View File

@ -21,8 +21,10 @@ type Course struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
CreatedAt time.Time `json:"created_at"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
// Populated on list-by-program. Practice count: lms_practices rows with course_id = course only
// (not practices attached to a module or lesson under this course).
@ -46,6 +48,7 @@ type CreateCourseInput struct {
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateCourseInput struct {
@ -54,4 +57,5 @@ type UpdateCourseInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -10,8 +10,10 @@ type ExamPrepCatalogCourse struct {
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
UnitsCount *int64 `json:"units_count,omitempty"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
UnitsCount *int64 `json:"units_count,omitempty"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"`
@ -32,6 +34,7 @@ type CreateExamPrepCatalogCourseInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateExamPrepCatalogCourseInput struct {
@ -41,4 +44,5 @@ type UpdateExamPrepCatalogCourseInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -11,8 +11,10 @@ type ExamPrepLesson struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
HasPractice bool `json:"has_practice"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
@ -30,6 +32,7 @@ type CreateExamPrepLessonInput struct {
Description *string `json:"description,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateExamPrepLessonInput struct {
@ -39,4 +42,5 @@ type UpdateExamPrepLessonInput struct {
Description *string `json:"description,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -11,8 +11,10 @@ type ExamPrepModule struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
@ -32,6 +34,7 @@ type CreateExamPrepModuleInput struct {
Icon *string `json:"icon,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateExamPrepModuleInput struct {
@ -41,4 +44,5 @@ type UpdateExamPrepModuleInput struct {
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -10,8 +10,10 @@ type ExamPrepUnit struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
ModulesCount *int64 `json:"modules_count,omitempty"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
@ -33,6 +35,7 @@ type CreateExamPrepUnitInput struct {
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateExamPrepUnitInput struct {
@ -41,4 +44,5 @@ type UpdateExamPrepUnitInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -40,8 +40,10 @@ type Lesson struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus LessonPublishStatus `json:"publish_status"`
HasPractice bool `json:"has_practice"`
PublishStatus LessonPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
@ -61,6 +63,7 @@ type CreateLessonInput struct {
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateLessonInput struct {
@ -70,4 +73,5 @@ type UpdateLessonInput struct {
Description *string `json:"description,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -11,8 +11,10 @@ type Module struct {
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
HasPractice bool `json:"has_practice"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
@ -31,6 +33,7 @@ type CreateModuleInput struct {
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateModuleInput struct {
@ -39,4 +42,5 @@ type UpdateModuleInput struct {
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -10,8 +10,10 @@ type Program struct {
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
CreatedAt time.Time `json:"created_at"`
PublishStatus ContentPublishStatus `json:"publish_status"`
AccessTier ContentAccessTier `json:"access_tier"`
EffectiveAccessTier ContentAccessTier `json:"effective_access_tier,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
}
@ -30,6 +32,7 @@ type CreateProgramInput struct {
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}
type UpdateProgramInput struct {
@ -39,4 +42,5 @@ type UpdateProgramInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
AccessTier *string `json:"access_tier,omitempty" validate:"omitempty,oneof=FREE free PREMIUM premium"`
}

View File

@ -18,6 +18,7 @@ func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPre
Category: c.Category,
SortOrder: int(c.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(c.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(c.AccessTier),
}
out.Description = fromPgText(c.Description)
out.Thumbnail = fromPgText(c.Thumbnail)
@ -36,6 +37,7 @@ func (s *Store) CreateExamPrepCatalogCourse(ctx context.Context, input domain.Cr
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.ExamPrepCatalogCourse{}, err
@ -61,6 +63,7 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
PublishStatus: c.PublishStatus,
AccessTier: c.AccessTier,
})
out.HasPractice = c.HasPractice
return out, nil
@ -94,6 +97,7 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, publishedOnly bo
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
item.UnitsCount = &r.UnitsCount
item.ModulesCount = &r.ModulesCount
@ -123,6 +127,7 @@ func (s *Store) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -18,6 +18,7 @@ func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLes
Title: l.Title,
SortOrder: int(l.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(l.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(l.AccessTier),
}
out.VideoURL = fromPgText(l.VideoUrl)
out.Thumbnail = fromPgText(l.Thumbnail)
@ -38,6 +39,7 @@ func (s *Store) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID
Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.ExamPrepLesson{}, err
@ -64,6 +66,7 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
PublishStatus: l.PublishStatus,
AccessTier: l.AccessTier,
})
out.HasPractice = l.HasPractice
return out, nil
@ -99,6 +102,7 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
item.HasPractice = r.HasPractice
out = append(out, item)
@ -129,6 +133,7 @@ func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, in
Description: optionalTextUpdate(input.Description),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -18,6 +18,7 @@ func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
Name: m.Name,
SortOrder: int(m.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(m.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(m.AccessTier),
}
out.Description = fromPgText(m.Description)
out.Thumbnail = fromPgText(m.Thumbnail)
@ -38,6 +39,7 @@ func (s *Store) CreateExamPrepUnitModule(ctx context.Context, unitID int64, inpu
Thumbnail: toPgText(input.Thumbnail),
Icon: toPgText(input.Icon),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.ExamPrepModule{}, err
@ -64,6 +66,7 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
PublishStatus: m.PublishStatus,
AccessTier: m.AccessTier,
})
out.HasPractice = m.HasPractice
return out, nil
@ -99,6 +102,7 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
item.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount
@ -131,6 +135,7 @@ func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input do
Icon: optionalTextUpdate(input.Icon),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -18,6 +18,7 @@ func examPrepUnitToDomain(u dbgen.ExamPrepUnit) domain.ExamPrepUnit {
Name: u.Name,
SortOrder: int(u.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(u.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(u.AccessTier),
}
out.Description = fromPgText(u.Description)
out.Thumbnail = fromPgText(u.Thumbnail)
@ -51,6 +52,7 @@ func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, i
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.ExamPrepUnit{}, err
@ -68,6 +70,7 @@ func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, i
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.ExamPrepUnit{}, err
@ -93,6 +96,7 @@ func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamP
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
PublishStatus: u.PublishStatus,
AccessTier: u.AccessTier,
})
out.HasPractice = u.HasPractice
return out, nil
@ -127,6 +131,7 @@ func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCou
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
item.ModulesCount = &r.ModulesCount
item.LessonsCount = &r.LessonsCount
@ -159,6 +164,7 @@ func (s *Store) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.U
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func courseToDomain(c dbgen.Course) domain.Course {
ProgramID: c.ProgramID,
Name: c.Name,
PublishStatus: domain.ContentPublishStatusFromDB(c.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(c.AccessTier),
}
out.Description = fromPgText(c.Description)
out.Thumbnail = fromPgText(c.Thumbnail)
@ -51,6 +52,7 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Course{}, err
@ -68,6 +70,7 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Course{}, err
@ -101,6 +104,7 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
PublishStatus: c.PublishStatus,
AccessTier: c.AccessTier,
})
out.HasPractice = c.HasPractice
return out, nil
@ -135,6 +139,7 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, pub
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
co.ModuleCount = int(r.ModuleCount)
co.LessonCount = int(r.LessonCount)
@ -177,6 +182,7 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
return domain.Course{}, err
@ -198,6 +204,7 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func lessonToDomain(l dbgen.Lesson) domain.Lesson {
ModuleID: l.ModuleID,
Title: l.Title,
PublishStatus: domain.LessonPublishStatusFromDB(l.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(l.AccessTier),
}
out.VideoURL = fromPgText(l.VideoUrl)
out.Thumbnail = fromPgText(l.Thumbnail)
@ -54,6 +55,7 @@ func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.C
Description: toPgText(input.Description),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Lesson{}, err
@ -72,6 +74,7 @@ func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.C
Description: toPgText(input.Description),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Lesson{}, err
@ -96,6 +99,7 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err
Description: l.Description,
SortOrder: l.SortOrder,
PublishStatus: l.PublishStatus,
AccessTier: l.AccessTier,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
})
@ -135,6 +139,7 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, publi
Description: r.Description,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
@ -179,6 +184,7 @@ func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateL
Description: optionalTextUpdate(input.Description),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pubParam,
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
return domain.Lesson{}, err
@ -201,6 +207,7 @@ func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateL
Description: optionalTextUpdate(input.Description),
SortOrder: sortParam,
PublishStatus: pubParam,
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -18,6 +18,7 @@ func moduleToDomain(m dbgen.Module) domain.Module {
CourseID: m.CourseID,
Name: m.Name,
PublishStatus: domain.ContentPublishStatusFromDB(m.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(m.AccessTier),
}
out.Description = fromPgText(m.Description)
out.Icon = fromPgText(m.Icon)
@ -53,6 +54,7 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Module{}, err
@ -71,6 +73,7 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Module{}, err
@ -105,6 +108,7 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
PublishStatus: m.PublishStatus,
AccessTier: m.AccessTier,
})
out.HasPractice = m.HasPractice
return out, nil
@ -141,6 +145,7 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
})
mod.HasPractice = r.HasPractice
out = append(out, mod)
@ -185,6 +190,7 @@ func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateM
Icon: optionalTextUpdate(input.Icon),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -18,6 +18,7 @@ func programToDomain(p dbgen.Program) domain.Program {
Name: p.Name,
Category: p.Category,
PublishStatus: domain.ContentPublishStatusFromDB(p.PublishStatus),
AccessTier: domain.ContentAccessTierFromDB(p.AccessTier),
}
out.Description = fromPgText(p.Description)
out.Thumbnail = fromPgText(p.Thumbnail)
@ -49,6 +50,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Program{}, err
@ -66,6 +68,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
AccessTier: contentAccessTierForCreate(input.AccessTier),
})
if err != nil {
return domain.Program{}, err
@ -117,6 +120,7 @@ func (s *Store) ListPrograms(ctx context.Context, publishedOnly bool, category s
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
AccessTier: r.AccessTier,
}))
}
return out, total, nil
@ -129,6 +133,23 @@ func optionalTextUpdate(val *string) pgtype.Text {
return pgtype.Text{String: *val, Valid: true}
}
func contentAccessTierForCreate(raw *string) string {
return string(domain.ContentAccessTierFromCreateInput(raw))
}
func optionalAccessTierUpdate(val *string) pgtype.Text {
if val == nil {
return pgtype.Text{Valid: false}
}
s := strings.TrimSpace(strings.ToUpper(*val))
switch s {
case string(domain.ContentAccessFree), string(domain.ContentAccessPremium):
return pgtype.Text{String: s, Valid: true}
default:
return pgtype.Text{Valid: false}
}
}
func optionalPublishStatusUpdate(val *string) pgtype.Text {
if val == nil {
return pgtype.Text{Valid: false}
@ -181,6 +202,7 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
return domain.Program{}, err
@ -207,6 +229,7 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
AccessTier: optionalAccessTierUpdate(input.AccessTier),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

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

View File

@ -0,0 +1,115 @@
package handlers
import (
"Yimaru-Backend/internal/domain"
"fmt"
"github.com/gofiber/fiber/v2"
)
func (h *Handler) learnerHasLearnEnglishSubscription(c *fiber.Ctx) (bool, error) {
return h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategoryLearnEnglish)
}
func (h *Handler) ensureLearnerPremiumContentAccess(c *fiber.Ctx, effectiveTier domain.ContentAccessTier, category domain.SubscriptionCategory) error {
role, _ := c.Locals("role").(domain.Role)
if !role.IsCustomerLearnerRole() || domain.CategorySubscriptionGateDisabled {
return nil
}
if !effectiveTier.RequiresSubscription() {
return nil
}
active, err := h.learnerHasSubscriptionCategory(c, category)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: fmt.Sprintf("Failed to verify %s subscription", category),
Error: err.Error(),
})
}
if !active {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: fmt.Sprintf("An active %s subscription is required", humanizeSubscriptionCategory(category)),
})
}
return nil
}
func applyProgramEffectiveAccessTier(p *domain.Program) {
p.EffectiveAccessTier = p.AccessTier
}
func applyCourseEffectiveAccessTier(course *domain.Course, program domain.Program) {
course.EffectiveAccessTier = domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier)
}
func applyModuleEffectiveAccessTier(module *domain.Module, program domain.Program, course domain.Course) {
module.EffectiveAccessTier = domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier)
}
func applyLessonEffectiveAccessTier(lesson *domain.Lesson, program domain.Program, course domain.Course, module domain.Module) {
lesson.EffectiveAccessTier = domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier, lesson.AccessTier)
}
func applyExamPrepCatalogCourseEffectiveAccessTier(cc *domain.ExamPrepCatalogCourse) {
cc.EffectiveAccessTier = cc.AccessTier
}
func applyExamPrepUnitEffectiveAccessTier(unit *domain.ExamPrepUnit, catalogCourse domain.ExamPrepCatalogCourse) {
unit.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier)
}
func applyExamPrepModuleEffectiveAccessTier(module *domain.ExamPrepModule, catalogCourse domain.ExamPrepCatalogCourse, unit domain.ExamPrepUnit) {
module.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier, module.AccessTier)
}
func applyExamPrepLessonEffectiveAccessTier(lesson *domain.ExamPrepLesson, catalogCourse domain.ExamPrepCatalogCourse, unit domain.ExamPrepUnit, module domain.ExamPrepModule) {
lesson.EffectiveAccessTier = domain.EffectiveContentAccessTier(catalogCourse.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier)
}
func learnerCanViewEffectiveTier(hasSubscription bool, effectiveTier domain.ContentAccessTier) bool {
return !effectiveTier.RequiresSubscription() || hasSubscription
}
func filterProgramsForLearner(items []domain.Program, hasLearnEnglish bool) []domain.Program {
filtered := make([]domain.Program, 0, len(items))
for _, item := range items {
applyProgramEffectiveAccessTier(&item)
if learnerCanViewEffectiveTier(hasLearnEnglish, item.EffectiveAccessTier) {
filtered = append(filtered, item)
}
}
return filtered
}
func filterCoursesForLearner(items []domain.Course, program domain.Program, hasLearnEnglish bool) []domain.Course {
filtered := make([]domain.Course, 0, len(items))
for i := range items {
applyCourseEffectiveAccessTier(&items[i], program)
if learnerCanViewEffectiveTier(hasLearnEnglish, items[i].EffectiveAccessTier) {
filtered = append(filtered, items[i])
}
}
return filtered
}
func filterExamPrepCatalogCoursesForLearner(items []domain.ExamPrepCatalogCourse, hasIELTS, hasDuolingo bool) []domain.ExamPrepCatalogCourse {
filtered := make([]domain.ExamPrepCatalogCourse, 0, len(items))
for _, item := range items {
applyExamPrepCatalogCourseEffectiveAccessTier(&item)
if !item.EffectiveAccessTier.RequiresSubscription() {
filtered = append(filtered, item)
continue
}
switch domain.SubscriptionCategory(item.Category) {
case domain.SubscriptionCategoryIELTS:
if hasIELTS {
filtered = append(filtered, item)
}
case domain.SubscriptionCategoryDuolingo:
if hasDuolingo {
filtered = append(filtered, item)
}
}
}
return filtered
}

View File

@ -93,6 +93,8 @@ func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
publishedOnly := !h.canManageLMSCourses(c)
role := c.Locals("role").(domain.Role)
var program domain.Program
if publishedOnly {
// Draft programs hide their courses from non-managers.
p, err := h.programSvc.GetByID(c.Context(), programID)
@ -117,6 +119,7 @@ func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
if err := h.blockLearnerIfNotLMSProgram(c, p); err != nil {
return err
}
program = p
}
items, total, err := h.courseSvc.ListByProgram(c.Context(), programID, publishedOnly, int32(limit), int32(offset))
if err != nil {
@ -131,8 +134,18 @@ func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if role.IsCustomerLearnerRole() && publishedOnly {
hasLearnEnglish, err := h.learnerHasLearnEnglishSubscription(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Learn English subscription",
Error: err.Error(),
})
}
items = filterCoursesForLearner(items, program, hasLearnEnglish)
total = int64(len(items))
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
for i := range items {
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &items[i]); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
@ -205,6 +218,10 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
if err := h.blockLearnerIfNotLMSProgram(c, p); err != nil {
return err
}
applyCourseEffectiveAccessTier(&course, p)
if err := h.ensureLearnerPremiumContentAccess(c, course.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
return err
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil {

View File

@ -220,7 +220,8 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
Error: examprep.ErrCatalogCourseNotFound.Error(),
})
}
if err := h.ensureLearnerExamPrepContentAccess(c, out.Category); err != nil {
applyExamPrepCatalogCourseEffectiveAccessTier(&out)
if err := h.ensureLearnerExamPrepContentAccess(c, out.Category, out.EffectiveAccessTier); err != nil {
return err
}
if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil {

View File

@ -82,6 +82,17 @@ func (h *Handler) ListPrograms(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if role.IsCustomerLearnerRole() {
hasLearnEnglish, err := h.learnerHasLearnEnglishSubscription(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Learn English subscription",
Error: err.Error(),
})
}
items = filterProgramsForLearner(items, hasLearnEnglish)
total = int64(len(items))
}
uid := c.Locals("user_id").(int64)
for i := range items {
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &items[i]); err != nil {
@ -142,6 +153,10 @@ func (h *Handler) GetProgram(c *fiber.Ctx) error {
if err := h.blockLearnerIfNotLMSProgram(c, p); err != nil {
return err
}
applyProgramEffectiveAccessTier(&p)
if err := h.ensureLearnerPremiumContentAccess(c, p.EffectiveAccessTier, domain.SubscriptionCategoryLearnEnglish); err != nil {
return err
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &p); err != nil {

View File

@ -2,7 +2,6 @@ package handlers
import (
"Yimaru-Backend/internal/domain"
"fmt"
"github.com/gofiber/fiber/v2"
)
@ -15,7 +14,7 @@ func (h *Handler) learnerHasSubscriptionCategory(c *fiber.Ctx, category domain.S
return h.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, category)
}
func (h *Handler) ensureLearnerExamPrepContentAccess(c *fiber.Ctx, contentCategory string) error {
func (h *Handler) ensureLearnerExamPrepContentAccess(c *fiber.Ctx, contentCategory string, effectiveTier domain.ContentAccessTier) error {
role, _ := c.Locals("role").(domain.Role)
if !role.IsCustomerLearnerRole() {
return nil
@ -25,19 +24,7 @@ func (h *Handler) ensureLearnerExamPrepContentAccess(c *fiber.Ctx, contentCatego
Message: "Catalog course not found",
})
}
active, err := h.learnerHasSubscriptionCategory(c, domain.SubscriptionCategory(contentCategory))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: fmt.Sprintf("Failed to verify %s subscription", contentCategory),
Error: err.Error(),
})
}
if !active {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: fmt.Sprintf("An active %s subscription is required", humanizeSubscriptionCategory(domain.SubscriptionCategory(contentCategory))),
})
}
return nil
return h.ensureLearnerPremiumContentAccess(c, effectiveTier, domain.SubscriptionCategory(contentCategory))
}
func (h *Handler) blockLearnerIfNotLMSProgram(c *fiber.Ctx, program domain.Program) error {
@ -66,19 +53,3 @@ func humanizeSubscriptionCategory(category domain.SubscriptionCategory) string {
}
}
func filterExamPrepCatalogCoursesForLearner(items []domain.ExamPrepCatalogCourse, hasIELTS, hasDuolingo bool) []domain.ExamPrepCatalogCourse {
filtered := make([]domain.ExamPrepCatalogCourse, 0, len(items))
for _, item := range items {
switch domain.SubscriptionCategory(item.Category) {
case domain.SubscriptionCategoryIELTS:
if hasIELTS {
filtered = append(filtered, item)
}
case domain.SubscriptionCategoryDuolingo:
if hasDuolingo {
filtered = append(filtered, item)
}
}
}
return filtered
}

View File

@ -282,17 +282,23 @@ func (a *App) RequireExamPrepSubscription() fiber.Handler {
}
if !scoped {
hasIELTS, err := a.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, domain.SubscriptionCategoryIELTS)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify subscription")
}
hasDuolingo, err := a.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, domain.SubscriptionCategoryDuolingo)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify subscription")
}
if !hasIELTS && !hasDuolingo {
return fiber.NewError(fiber.StatusForbidden, "An active IELTS or Duolingo subscription is required")
return c.Next()
}
tier, tierResolved, err := a.resolveExamPrepEffectiveAccessTier(c)
if err != nil {
switch {
case errors.Is(err, examprepsvc.ErrCatalogCourseNotFound),
errors.Is(err, examprepsvc.ErrUnitNotFound),
errors.Is(err, examprepsvc.ErrModuleNotFound),
errors.Is(err, examprepsvc.ErrLessonNotFound),
errors.Is(err, examprepsvc.ErrPracticeNotFound):
return fiber.NewError(fiber.StatusNotFound, err.Error())
default:
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify content access")
}
}
if tierResolved && !tier.RequiresSubscription() {
return c.Next()
}

View File

@ -84,11 +84,11 @@ func (a *App) initAppRoutes() {
// Programs (LMS top-level)
groupV1.Post("/programs", a.authMiddleware, a.RequirePermission("programs.create"), h.CreateProgram)
groupV1.Get("/programs", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("programs.list"), h.ListPrograms)
groupV1.Get("/programs", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("programs.list"), h.ListPrograms)
groupV1.Put("/programs/reorder", a.authMiddleware, a.RequirePermission("programs.reorder"), h.ReorderPrograms)
groupV1.Get("/lms/progress", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
groupV1.Get("/lms/progress-summary", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgressSummary)
groupV1.Get("/programs/:id", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("programs.get"), h.GetProgram)
groupV1.Get("/lms/progress", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
groupV1.Get("/lms/progress-summary", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgressSummary)
groupV1.Get("/programs/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("programs.get"), h.GetProgram)
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
@ -133,33 +133,33 @@ func (a *App) initAppRoutes() {
// Courses
groupV1.Post("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
groupV1.Put("/programs/:id/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCoursesInProgram)
groupV1.Get("/programs/:id/courses", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("courses.list_by_program"), h.ListCoursesByProgram)
groupV1.Get("/courses/:id/practices", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("practices.list"), h.ListPracticesByCourse)
groupV1.Get("/courses/:id", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("courses.get"), h.GetCourse)
groupV1.Get("/programs/:id/courses", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("courses.list_by_program"), h.ListCoursesByProgram)
groupV1.Get("/courses/:id/practices", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.list"), h.ListPracticesByCourse)
groupV1.Get("/courses/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("courses.get"), h.GetCourse)
groupV1.Put("/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
groupV1.Delete("/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
groupV1.Post("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.create"), h.CreateModule)
groupV1.Put("/courses/:courseId/modules/reorder", a.authMiddleware, a.RequirePermission("modules.reorder"), h.ReorderModulesInCourse)
groupV1.Get("/courses/:courseId/modules", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("modules.list_by_course"), h.ListModulesByCourse)
groupV1.Get("/courses/:courseId/modules", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("modules.list_by_course"), h.ListModulesByCourse)
// /modules/:moduleId/lessons before /modules/:id; /modules/:id/practices before /modules/:id
groupV1.Post("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.create"), h.CreateLesson)
groupV1.Get("/modules/:moduleId/lessons", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("lessons.list_by_module"), h.ListLessonsByModule)
groupV1.Get("/modules/:moduleId/lessons", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("lessons.list_by_module"), h.ListLessonsByModule)
groupV1.Put("/modules/:moduleId/lessons/reorder", a.authMiddleware, a.RequirePermission("lessons.reorder"), h.ReorderLessonsInModule)
groupV1.Get("/modules/:id/practices", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("practices.list"), h.ListPracticesByModule)
groupV1.Get("/modules/:id", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("modules.get"), h.GetModule)
groupV1.Get("/modules/:id/practices", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.list"), h.ListPracticesByModule)
groupV1.Get("/modules/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("modules.get"), h.GetModule)
groupV1.Put("/modules/:id", a.authMiddleware, a.RequirePermission("modules.update"), h.UpdateModule)
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("practices.list"), h.ListPracticesByLesson)
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("lessons.complete"), h.CompleteLesson)
groupV1.Post("/videos/engagement/heartbeat", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("videos.track_engagement"), h.RecordVideoEngagementHeartbeat)
groupV1.Post("/progress/practices/:id/complete", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("progress.complete"), h.CompletePractice)
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("lessons.get"), h.GetLesson)
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.list"), h.ListPracticesByLesson)
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("lessons.complete"), h.CompleteLesson)
groupV1.Post("/videos/engagement/heartbeat", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("videos.track_engagement"), h.RecordVideoEngagementHeartbeat)
groupV1.Post("/progress/practices/:id/complete", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("progress.complete"), h.CompletePractice)
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("lessons.get"), h.GetLesson)
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
groupV1.Post("/practices", a.authMiddleware, a.RequirePermission("practices.create"), h.CreatePractice)
groupV1.Get("/practices/:id", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("practices.get"), h.GetPractice)
groupV1.Get("/practices/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.get"), h.GetPractice)
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
groupV1.Put("/practices/:id/full", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdateLmsPracticeFull)
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
@ -250,7 +250,7 @@ func (a *App) initAppRoutes() {
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
groupV1.Get("/question-sets/:setId/question-types", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionTypesInSet)
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)
groupV1.Put("/question-sets/:setId/questions/:questionId/order", a.authMiddleware, a.RequirePermission("question_set_items.update_order"), h.UpdateQuestionOrderInSet)