Yimaru-BackEnd/internal/repository/progression.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,
}
}