311 lines
9.7 KiB
Go
311 lines
9.7 KiB
Go
package repository
|
|
|
|
import (
|
|
dbgen "Yimaru-Backend/gen/db"
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func NewProgressionStore(s *Store) ports.ProgressionStore { return s }
|
|
|
|
func (s *Store) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
|
_, err := s.queries.AddSubCoursePrerequisite(ctx, dbgen.AddSubCoursePrerequisiteParams{
|
|
SubCourseID: subCourseID,
|
|
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Store) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
|
return s.queries.RemoveSubCoursePrerequisite(ctx, dbgen.RemoveSubCoursePrerequisiteParams{
|
|
SubCourseID: subCourseID,
|
|
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
|
})
|
|
}
|
|
|
|
func (s *Store) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) {
|
|
rows, err := s.queries.GetSubCoursePrerequisites(ctx, subCourseID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prereqs := make([]domain.SubCoursePrerequisite, len(rows))
|
|
for i, row := range rows {
|
|
prereqs[i] = domain.SubCoursePrerequisite{
|
|
ID: row.ID,
|
|
SubCourseID: row.SubCourseID,
|
|
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
|
CreatedAt: row.CreatedAt.Time,
|
|
PrerequisiteTitle: row.PrerequisiteTitle,
|
|
PrerequisiteLevel: row.PrerequisiteLevel,
|
|
PrerequisiteDisplayOrder: row.PrerequisiteDisplayOrder,
|
|
}
|
|
}
|
|
return prereqs, nil
|
|
}
|
|
|
|
func (s *Store) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) {
|
|
rows, err := s.queries.GetSubCourseDependents(ctx, prerequisiteSubCourseID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deps := make([]domain.SubCourseDependent, len(rows))
|
|
for i, row := range rows {
|
|
deps[i] = domain.SubCourseDependent{
|
|
ID: row.ID,
|
|
SubCourseID: row.SubCourseID,
|
|
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
|
CreatedAt: row.CreatedAt.Time,
|
|
DependentTitle: row.DependentTitle,
|
|
DependentLevel: row.DependentLevel,
|
|
}
|
|
}
|
|
return deps, nil
|
|
}
|
|
|
|
func (s *Store) CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error) {
|
|
return s.queries.CountUnmetPrerequisites(ctx, dbgen.CountUnmetPrerequisitesParams{
|
|
SubCourseID: subCourseID,
|
|
UserID: userID,
|
|
})
|
|
}
|
|
|
|
func (s *Store) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error {
|
|
return s.queries.DeleteAllPrerequisitesForSubCourse(ctx, subCourseID)
|
|
}
|
|
|
|
func (s *Store) StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
|
row, err := s.queries.StartSubCourseProgress(ctx, dbgen.StartSubCourseProgressParams{
|
|
UserID: userID,
|
|
SubCourseID: subCourseID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return s.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
|
}
|
|
return domain.UserSubCourseProgress{}, err
|
|
}
|
|
return mapUserSubCourseProgress(row), nil
|
|
}
|
|
|
|
func (s *Store) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error {
|
|
return s.queries.UpdateSubCourseProgress(ctx, dbgen.UpdateSubCourseProgressParams{
|
|
ProgressPercentage: percentage,
|
|
UserID: userID,
|
|
SubCourseID: subCourseID,
|
|
})
|
|
}
|
|
|
|
func (s *Store) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error {
|
|
return s.queries.CompleteSubCourse(ctx, dbgen.CompleteSubCourseParams{
|
|
UserID: userID,
|
|
SubCourseID: subCourseID,
|
|
})
|
|
}
|
|
|
|
func (s *Store) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
|
const query = `
|
|
WITH totals AS (
|
|
SELECT
|
|
(SELECT COUNT(*)::INT
|
|
FROM sub_course_videos v
|
|
WHERE v.sub_course_id = $2
|
|
AND v.status = 'PUBLISHED') AS total_videos,
|
|
(SELECT COUNT(*)::INT
|
|
FROM question_sets qs
|
|
WHERE qs.owner_type = 'SUB_COURSE'
|
|
AND qs.owner_id = $2
|
|
AND qs.set_type = 'PRACTICE'
|
|
AND qs.status = 'PUBLISHED') AS total_practices
|
|
),
|
|
completed AS (
|
|
SELECT
|
|
(SELECT COUNT(*)::INT
|
|
FROM user_sub_course_video_progress uv
|
|
JOIN sub_course_videos v ON v.id = uv.video_id
|
|
WHERE uv.user_id = $1
|
|
AND uv.sub_course_id = $2
|
|
AND uv.completed_at IS NOT NULL
|
|
AND v.status = 'PUBLISHED') AS completed_videos,
|
|
(SELECT COUNT(*)::INT
|
|
FROM user_practice_progress up
|
|
JOIN question_sets qs ON qs.id = up.question_set_id
|
|
WHERE up.user_id = $1
|
|
AND up.sub_course_id = $2
|
|
AND up.completed_at IS NOT NULL
|
|
AND qs.owner_type = 'SUB_COURSE'
|
|
AND qs.owner_id = $2
|
|
AND qs.set_type = 'PRACTICE'
|
|
AND qs.status = 'PUBLISHED') AS completed_practices
|
|
),
|
|
stats AS (
|
|
SELECT
|
|
(total_videos + total_practices) AS total_items,
|
|
(completed_videos + completed_practices) AS completed_items
|
|
FROM totals, completed
|
|
)
|
|
INSERT INTO user_sub_course_progress (
|
|
user_id,
|
|
sub_course_id,
|
|
status,
|
|
progress_percentage,
|
|
started_at,
|
|
completed_at,
|
|
updated_at
|
|
)
|
|
SELECT
|
|
$1,
|
|
$2,
|
|
CASE
|
|
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN 'COMPLETED'
|
|
WHEN stats.completed_items > 0 THEN 'IN_PROGRESS'
|
|
ELSE 'NOT_STARTED'
|
|
END,
|
|
CASE
|
|
WHEN stats.total_items = 0 THEN 0
|
|
ELSE ROUND((stats.completed_items::NUMERIC * 100.0) / stats.total_items::NUMERIC)::SMALLINT
|
|
END,
|
|
CASE
|
|
WHEN stats.completed_items > 0 THEN CURRENT_TIMESTAMP
|
|
ELSE NULL
|
|
END,
|
|
CASE
|
|
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN CURRENT_TIMESTAMP
|
|
ELSE NULL
|
|
END,
|
|
CURRENT_TIMESTAMP
|
|
FROM stats
|
|
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
|
status = EXCLUDED.status,
|
|
progress_percentage = EXCLUDED.progress_percentage,
|
|
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
|
completed_at = EXCLUDED.completed_at,
|
|
updated_at = EXCLUDED.updated_at;
|
|
`
|
|
|
|
_, err := s.conn.Exec(ctx, query, userID, subCourseID)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
|
row, err := s.queries.GetUserSubCourseProgress(ctx, dbgen.GetUserSubCourseProgressParams{
|
|
UserID: userID,
|
|
SubCourseID: subCourseID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.UserSubCourseProgress{}, domain.ErrProgressNotFound
|
|
}
|
|
return domain.UserSubCourseProgress{}, err
|
|
}
|
|
return mapUserSubCourseProgress(row), nil
|
|
}
|
|
|
|
func (s *Store) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) {
|
|
rows, err := s.queries.GetUserCourseProgress(ctx, dbgen.GetUserCourseProgressParams{
|
|
UserID: userID,
|
|
CourseID: courseID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]domain.UserCourseProgressItem, len(rows))
|
|
for i, row := range rows {
|
|
var startedAt, completedAt *time.Time
|
|
if row.StartedAt.Valid {
|
|
startedAt = &row.StartedAt.Time
|
|
}
|
|
if row.CompletedAt.Valid {
|
|
completedAt = &row.CompletedAt.Time
|
|
}
|
|
var updatedAt *time.Time
|
|
if row.UpdatedAt.Valid {
|
|
updatedAt = &row.UpdatedAt.Time
|
|
}
|
|
items[i] = domain.UserCourseProgressItem{
|
|
ID: row.ID,
|
|
UserID: row.UserID,
|
|
SubCourseID: row.SubCourseID,
|
|
Status: domain.ProgressStatus(row.Status),
|
|
ProgressPercentage: row.ProgressPercentage,
|
|
StartedAt: startedAt,
|
|
CompletedAt: completedAt,
|
|
CreatedAt: row.CreatedAt.Time,
|
|
UpdatedAt: updatedAt,
|
|
SubCourseTitle: row.SubCourseTitle,
|
|
SubCourseLevel: row.SubCourseLevel,
|
|
SubCourseDisplayOrder: row.SubCourseDisplayOrder,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Store) GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) {
|
|
rows, err := s.queries.GetSubCoursesWithProgressByCourse(ctx, dbgen.GetSubCoursesWithProgressByCourseParams{
|
|
UserID: userID,
|
|
CourseID: courseID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]domain.SubCourseWithProgress, len(rows))
|
|
for i, row := range rows {
|
|
var startedAt, completedAt *time.Time
|
|
if row.StartedAt.Valid {
|
|
startedAt = &row.StartedAt.Time
|
|
}
|
|
if row.CompletedAt.Valid {
|
|
completedAt = &row.CompletedAt.Time
|
|
}
|
|
items[i] = domain.SubCourseWithProgress{
|
|
SubCourseID: row.SubCourseID,
|
|
Title: row.Title,
|
|
Description: ptrText(row.Description),
|
|
Thumbnail: ptrText(row.Thumbnail),
|
|
DisplayOrder: row.DisplayOrder,
|
|
Level: row.Level,
|
|
IsActive: row.IsActive,
|
|
ProgressStatus: domain.ProgressStatus(row.ProgressStatus),
|
|
ProgressPercentage: row.ProgressPercentage,
|
|
StartedAt: startedAt,
|
|
CompletedAt: completedAt,
|
|
UnmetPrerequisitesCount: row.UnmetPrerequisitesCount,
|
|
IsLocked: row.UnmetPrerequisitesCount > 0,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func mapUserSubCourseProgress(row dbgen.UserSubCourseProgress) domain.UserSubCourseProgress {
|
|
var startedAt, completedAt *time.Time
|
|
if row.StartedAt.Valid {
|
|
startedAt = &row.StartedAt.Time
|
|
}
|
|
if row.CompletedAt.Valid {
|
|
completedAt = &row.CompletedAt.Time
|
|
}
|
|
var updatedAt *time.Time
|
|
if row.UpdatedAt.Valid {
|
|
updatedAt = &row.UpdatedAt.Time
|
|
}
|
|
return domain.UserSubCourseProgress{
|
|
ID: row.ID,
|
|
UserID: row.UserID,
|
|
SubCourseID: row.SubCourseID,
|
|
Status: domain.ProgressStatus(row.Status),
|
|
ProgressPercentage: row.ProgressPercentage,
|
|
StartedAt: startedAt,
|
|
CompletedAt: completedAt,
|
|
CreatedAt: row.CreatedAt.Time,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
}
|