package academy import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // ============================================================================ // Enrollment Operations // ============================================================================ // CreateEnrollment creates a new enrollment func (s *Store) CreateEnrollment(ctx context.Context, enrollment *Enrollment) error { enrollment.ID = uuid.New() enrollment.CreatedAt = time.Now().UTC() enrollment.UpdatedAt = enrollment.CreatedAt if enrollment.Status == "" { enrollment.Status = EnrollmentStatusNotStarted } _, err := s.pool.Exec(ctx, ` INSERT INTO academy_enrollments ( id, tenant_id, course_id, user_id, user_name, user_email, status, progress_percent, current_lesson_index, started_at, completed_at, deadline, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 ) `, enrollment.ID, enrollment.TenantID, enrollment.CourseID, enrollment.UserID, enrollment.UserName, enrollment.UserEmail, string(enrollment.Status), enrollment.ProgressPercent, enrollment.CurrentLessonIndex, enrollment.StartedAt, enrollment.CompletedAt, enrollment.Deadline, enrollment.CreatedAt, enrollment.UpdatedAt, ) return err } // GetEnrollment retrieves an enrollment by ID func (s *Store) GetEnrollment(ctx context.Context, id uuid.UUID) (*Enrollment, error) { var enrollment Enrollment var status string err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, course_id, user_id, user_name, user_email, status, progress_percent, current_lesson_index, started_at, completed_at, deadline, created_at, updated_at FROM academy_enrollments WHERE id = $1 `, id).Scan( &enrollment.ID, &enrollment.TenantID, &enrollment.CourseID, &enrollment.UserID, &enrollment.UserName, &enrollment.UserEmail, &status, &enrollment.ProgressPercent, &enrollment.CurrentLessonIndex, &enrollment.StartedAt, &enrollment.CompletedAt, &enrollment.Deadline, &enrollment.CreatedAt, &enrollment.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } enrollment.Status = EnrollmentStatus(status) return &enrollment, nil } // ListEnrollments lists enrollments for a tenant with optional filters func (s *Store) ListEnrollments(ctx context.Context, tenantID uuid.UUID, filters *EnrollmentFilters) ([]Enrollment, int, error) { // Count query countQuery := "SELECT COUNT(*) FROM academy_enrollments WHERE tenant_id = $1" countArgs := []interface{}{tenantID} countArgIdx := 2 // List query query := ` SELECT id, tenant_id, course_id, user_id, user_name, user_email, status, progress_percent, current_lesson_index, started_at, completed_at, deadline, created_at, updated_at FROM academy_enrollments WHERE tenant_id = $1` args := []interface{}{tenantID} argIdx := 2 if filters != nil { if filters.CourseID != nil { query += fmt.Sprintf(" AND course_id = $%d", argIdx) args = append(args, *filters.CourseID) argIdx++ countQuery += fmt.Sprintf(" AND course_id = $%d", countArgIdx) countArgs = append(countArgs, *filters.CourseID) countArgIdx++ } if filters.UserID != nil { query += fmt.Sprintf(" AND user_id = $%d", argIdx) args = append(args, *filters.UserID) argIdx++ countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx) countArgs = append(countArgs, *filters.UserID) countArgIdx++ } if filters.Status != "" { query += fmt.Sprintf(" AND status = $%d", argIdx) args = append(args, string(filters.Status)) argIdx++ countQuery += fmt.Sprintf(" AND status = $%d", countArgIdx) countArgs = append(countArgs, string(filters.Status)) countArgIdx++ } } // Get total count var total int err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total) if err != nil { return nil, 0, err } query += " ORDER BY created_at DESC" if filters != nil && filters.Limit > 0 { query += fmt.Sprintf(" LIMIT $%d", argIdx) args = append(args, filters.Limit) argIdx++ if filters.Offset > 0 { query += fmt.Sprintf(" OFFSET $%d", argIdx) args = append(args, filters.Offset) argIdx++ } } rows, err := s.pool.Query(ctx, query, args...) if err != nil { return nil, 0, err } defer rows.Close() var enrollments []Enrollment for rows.Next() { var enrollment Enrollment var status string err := rows.Scan( &enrollment.ID, &enrollment.TenantID, &enrollment.CourseID, &enrollment.UserID, &enrollment.UserName, &enrollment.UserEmail, &status, &enrollment.ProgressPercent, &enrollment.CurrentLessonIndex, &enrollment.StartedAt, &enrollment.CompletedAt, &enrollment.Deadline, &enrollment.CreatedAt, &enrollment.UpdatedAt, ) if err != nil { return nil, 0, err } enrollment.Status = EnrollmentStatus(status) enrollments = append(enrollments, enrollment) } if enrollments == nil { enrollments = []Enrollment{} } return enrollments, total, nil } // UpdateEnrollmentProgress updates the progress for an enrollment func (s *Store) UpdateEnrollmentProgress(ctx context.Context, id uuid.UUID, progress int, currentLesson int) error { now := time.Now().UTC() _, err := s.pool.Exec(ctx, ` UPDATE academy_enrollments SET progress_percent = $2, current_lesson_index = $3, status = CASE WHEN $2 >= 100 THEN 'completed' WHEN $2 > 0 THEN 'in_progress' ELSE status END, started_at = CASE WHEN started_at IS NULL AND $2 > 0 THEN $4 ELSE started_at END, completed_at = CASE WHEN $2 >= 100 THEN $4 ELSE completed_at END, updated_at = $4 WHERE id = $1 `, id, progress, currentLesson, now) return err } // CompleteEnrollment marks an enrollment as completed func (s *Store) CompleteEnrollment(ctx context.Context, id uuid.UUID) error { now := time.Now().UTC() _, err := s.pool.Exec(ctx, ` UPDATE academy_enrollments SET status = 'completed', progress_percent = 100, completed_at = $2, updated_at = $2 WHERE id = $1 `, id, now) return err } // ============================================================================ // Certificate Operations // ============================================================================ // GetCertificate retrieves a certificate by ID func (s *Store) GetCertificate(ctx context.Context, id uuid.UUID) (*Certificate, error) { var cert Certificate err := s.pool.QueryRow(ctx, ` SELECT id, enrollment_id, user_name, course_title, issued_at, valid_until, pdf_url FROM academy_certificates WHERE id = $1 `, id).Scan( &cert.ID, &cert.EnrollmentID, &cert.UserName, &cert.CourseTitle, &cert.IssuedAt, &cert.ValidUntil, &cert.PDFURL, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &cert, nil } // GetCertificateByEnrollment retrieves a certificate by enrollment ID func (s *Store) GetCertificateByEnrollment(ctx context.Context, enrollmentID uuid.UUID) (*Certificate, error) { var cert Certificate err := s.pool.QueryRow(ctx, ` SELECT id, enrollment_id, user_name, course_title, issued_at, valid_until, pdf_url FROM academy_certificates WHERE enrollment_id = $1 `, enrollmentID).Scan( &cert.ID, &cert.EnrollmentID, &cert.UserName, &cert.CourseTitle, &cert.IssuedAt, &cert.ValidUntil, &cert.PDFURL, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &cert, nil } // CreateCertificate creates a new certificate func (s *Store) CreateCertificate(ctx context.Context, cert *Certificate) error { cert.ID = uuid.New() cert.IssuedAt = time.Now().UTC() _, err := s.pool.Exec(ctx, ` INSERT INTO academy_certificates ( id, enrollment_id, user_name, course_title, issued_at, valid_until, pdf_url ) VALUES ( $1, $2, $3, $4, $5, $6, $7 ) `, cert.ID, cert.EnrollmentID, cert.UserName, cert.CourseTitle, cert.IssuedAt, cert.ValidUntil, cert.PDFURL, ) return err } // ============================================================================ // Statistics // ============================================================================ // GetStatistics returns aggregated academy statistics for a tenant func (s *Store) GetStatistics(ctx context.Context, tenantID uuid.UUID) (*AcademyStatistics, error) { stats := &AcademyStatistics{} // Total active courses s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM academy_courses WHERE tenant_id = $1 AND is_active = true", tenantID).Scan(&stats.TotalCourses) // Total enrollments s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM academy_enrollments WHERE tenant_id = $1", tenantID).Scan(&stats.TotalEnrollments) // Completion rate if stats.TotalEnrollments > 0 { var completed int s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM academy_enrollments WHERE tenant_id = $1 AND status = 'completed'", tenantID).Scan(&completed) stats.CompletionRate = float64(completed) / float64(stats.TotalEnrollments) * 100 } // Overdue count (past deadline, not completed) s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM academy_enrollments WHERE tenant_id = $1 AND status NOT IN ('completed', 'expired') AND deadline IS NOT NULL AND deadline < NOW()`, tenantID).Scan(&stats.OverdueCount) // Average completion days s.pool.QueryRow(ctx, `SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) / 86400), 0) FROM academy_enrollments WHERE tenant_id = $1 AND status = 'completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL`, tenantID).Scan(&stats.AvgCompletionDays) return stats, nil }