package handlers import ( "net/http" "strconv" "time" "github.com/breakpilot/ai-compliance-sdk/internal/academy" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ============================================================================ // Enrollment Management // ============================================================================ // CreateEnrollment enrolls a user in a course // POST /sdk/v1/academy/enrollments func (h *AcademyHandlers) CreateEnrollment(c *gin.Context) { var req academy.EnrollUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) // Verify course exists course, err := h.store.GetCourse(c.Request.Context(), req.CourseID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if course == nil { c.JSON(http.StatusNotFound, gin.H{"error": "course not found"}) return } enrollment := &academy.Enrollment{ TenantID: tenantID, CourseID: req.CourseID, UserID: req.UserID, UserName: req.UserName, UserEmail: req.UserEmail, Status: academy.EnrollmentStatusNotStarted, Deadline: req.Deadline, } if err := h.store.CreateEnrollment(c.Request.Context(), enrollment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"enrollment": enrollment}) } // ListEnrollments lists enrollments for the current tenant // GET /sdk/v1/academy/enrollments func (h *AcademyHandlers) ListEnrollments(c *gin.Context) { tenantID := rbac.GetTenantID(c) filters := &academy.EnrollmentFilters{ Limit: 50, } if status := c.Query("status"); status != "" { filters.Status = academy.EnrollmentStatus(status) } if courseIDStr := c.Query("course_id"); courseIDStr != "" { if courseID, err := uuid.Parse(courseIDStr); err == nil { filters.CourseID = &courseID } } if userIDStr := c.Query("user_id"); userIDStr != "" { if userID, err := uuid.Parse(userIDStr); err == nil { filters.UserID = &userID } } if limitStr := c.Query("limit"); limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { filters.Limit = limit } } if offsetStr := c.Query("offset"); offsetStr != "" { if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { filters.Offset = offset } } enrollments, total, err := h.store.ListEnrollments(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, academy.EnrollmentListResponse{ Enrollments: enrollments, Total: total, }) } // UpdateProgress updates an enrollment's progress // PUT /sdk/v1/academy/enrollments/:id/progress func (h *AcademyHandlers) UpdateProgress(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid enrollment ID"}) return } enrollment, err := h.store.GetEnrollment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if enrollment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "enrollment not found"}) return } var req academy.UpdateProgressRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Progress < 0 || req.Progress > 100 { c.JSON(http.StatusBadRequest, gin.H{"error": "progress must be between 0 and 100"}) return } if err := h.store.UpdateEnrollmentProgress(c.Request.Context(), id, req.Progress, req.CurrentLesson); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Fetch updated enrollment updated, err := h.store.GetEnrollment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"enrollment": updated}) } // CompleteEnrollment marks an enrollment as completed // POST /sdk/v1/academy/enrollments/:id/complete func (h *AcademyHandlers) CompleteEnrollment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid enrollment ID"}) return } enrollment, err := h.store.GetEnrollment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if enrollment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "enrollment not found"}) return } if enrollment.Status == academy.EnrollmentStatusCompleted { c.JSON(http.StatusBadRequest, gin.H{"error": "enrollment already completed"}) return } if err := h.store.CompleteEnrollment(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Fetch updated enrollment updated, err := h.store.GetEnrollment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "enrollment": updated, "message": "enrollment completed", }) } // ============================================================================ // Certificate Management // ============================================================================ // GetCertificate retrieves a certificate // GET /sdk/v1/academy/certificates/:id func (h *AcademyHandlers) GetCertificate(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"}) return } cert, err := h.store.GetCertificate(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } c.JSON(http.StatusOK, gin.H{"certificate": cert}) } // GenerateCertificate generates a certificate for a completed enrollment // POST /sdk/v1/academy/enrollments/:id/certificate func (h *AcademyHandlers) GenerateCertificate(c *gin.Context) { enrollmentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid enrollment ID"}) return } enrollment, err := h.store.GetEnrollment(c.Request.Context(), enrollmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if enrollment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "enrollment not found"}) return } if enrollment.Status != academy.EnrollmentStatusCompleted { c.JSON(http.StatusBadRequest, gin.H{"error": "enrollment must be completed before generating certificate"}) return } // Check if certificate already exists existing, err := h.store.GetCertificateByEnrollment(c.Request.Context(), enrollmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if existing != nil { c.JSON(http.StatusOK, gin.H{"certificate": existing, "message": "certificate already exists"}) return } // Get the course for the certificate title course, err := h.store.GetCourse(c.Request.Context(), enrollment.CourseID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } courseTitle := "Unknown Course" if course != nil { courseTitle = course.Title } // Certificate is valid for 1 year by default validUntil := time.Now().UTC().AddDate(1, 0, 0) cert := &academy.Certificate{ EnrollmentID: enrollmentID, UserName: enrollment.UserName, CourseTitle: courseTitle, ValidUntil: &validUntil, } if err := h.store.CreateCertificate(c.Request.Context(), cert); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"certificate": cert}) } // DownloadCertificatePDF generates and downloads a certificate as PDF // GET /sdk/v1/academy/certificates/:id/pdf func (h *AcademyHandlers) DownloadCertificatePDF(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"}) return } cert, err := h.store.GetCertificate(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if cert == nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } validUntil := time.Now().UTC().AddDate(1, 0, 0) if cert.ValidUntil != nil { validUntil = *cert.ValidUntil } pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{ CertificateID: cert.ID.String(), UserName: cert.UserName, CourseName: cert.CourseTitle, IssuedAt: cert.IssuedAt, ValidUntil: validUntil, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate PDF: " + err.Error()}) return } shortID := cert.ID.String()[:8] c.Header("Content-Disposition", "attachment; filename=zertifikat-"+shortID+".pdf") c.Data(http.StatusOK, "application/pdf", pdfBytes) }