free/premium feature
This commit is contained in:
parent
26cf7d2908
commit
c9a4bc1306
23
db/migrations/000081_content_access_tier.down.sql
Normal file
23
db/migrations/000081_content_access_tier.down.sql
Normal 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;
|
||||
34
db/migrations/000081_content_access_tier.up.sql
Normal file
34
db/migrations/000081_content_access_tier.up.sql
Normal 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'));
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
45
internal/domain/content_access_tier.go
Normal file
45
internal/domain/content_access_tier.go
Normal 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
|
||||
}
|
||||
15
internal/domain/content_access_tier_test.go
Normal file
15
internal/domain/content_access_tier_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
335
internal/web_server/content_access_middleware.go
Normal file
335
internal/web_server/content_access_middleware.go
Normal 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
|
||||
}
|
||||
}
|
||||
115
internal/web_server/handlers/content_access_gate.go
Normal file
115
internal/web_server/handlers/content_access_gate.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user