Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/training_handlers_stats.go
Sharang Parnerkar 3f306fb6f0 refactor(go/handlers): split iace_handler and training_handlers into focused files
iace_handler.go (2706 LOC) split into 9 files:
- iace_handler.go: struct, constructor, shared helpers (~156 LOC)
- iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC)
- iace_handler_components.go: components + classification (~387 LOC)
- iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC)
- iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC)
- iace_handler_techfile.go: CE tech file generation/export (~452 LOC)
- iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC)
- iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC)
- iace_handler_rag.go: RAG library search + section enrichment (~142 LOC)

training_handlers.go (1864 LOC) split into 9 files:
- training_handlers.go: struct + constructor (~23 LOC)
- training_handlers_modules.go: module CRUD (~226 LOC)
- training_handlers_matrix.go: CTM matrix endpoints (~95 LOC)
- training_handlers_assignments.go: assignment lifecycle (~243 LOC)
- training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC)
- training_handlers_content.go: LLM content/audio/video generation (~274 LOC)
- training_handlers_media.go: media, streaming, interactive video (~325 LOC)
- training_handlers_blocks.go: block configs + canonical controls (~280 LOC)
- training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC)

All files remain in package handlers. Zero behavior changes. All exported
function names preserved. All files under 500 LOC hard cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:17:20 +02:00

291 lines
8.1 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/academy"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/training"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ============================================================================
// Deadline / Escalation Endpoints
// ============================================================================
// GetDeadlines returns upcoming deadlines
// GET /sdk/v1/training/deadlines
func (h *TrainingHandlers) GetDeadlines(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
limit := 20
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
limit = n
}
}
deadlines, err := h.store.GetDeadlines(c.Request.Context(), tenantID, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, training.DeadlineListResponse{
Deadlines: deadlines,
Total: len(deadlines),
})
}
// GetOverdueDeadlines returns overdue assignments
// GET /sdk/v1/training/deadlines/overdue
func (h *TrainingHandlers) GetOverdueDeadlines(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
deadlines, err := training.GetOverdueDeadlines(c.Request.Context(), h.store, tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, training.DeadlineListResponse{
Deadlines: deadlines,
Total: len(deadlines),
})
}
// CheckEscalation runs the escalation check
// POST /sdk/v1/training/escalation/check
func (h *TrainingHandlers) CheckEscalation(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
results, err := training.CheckEscalations(c.Request.Context(), h.store, tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
overdueAll, _ := h.store.ListOverdueAssignments(c.Request.Context(), tenantID)
c.JSON(http.StatusOK, training.EscalationResponse{
Results: results,
TotalChecked: len(overdueAll),
Escalated: len(results),
})
}
// ============================================================================
// Audit / Stats Endpoints
// ============================================================================
// GetAuditLog returns the training audit trail
// GET /sdk/v1/training/audit-log
func (h *TrainingHandlers) GetAuditLog(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
filters := &training.AuditLogFilters{
Limit: 50,
Offset: 0,
}
if v := c.Query("action"); v != "" {
filters.Action = training.AuditAction(v)
}
if v := c.Query("entity_type"); v != "" {
filters.EntityType = training.AuditEntityType(v)
}
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
filters.Limit = n
}
}
if v := c.Query("offset"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
filters.Offset = n
}
}
entries, total, err := h.store.ListAuditLog(c.Request.Context(), tenantID, filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, training.AuditLogResponse{
Entries: entries,
Total: total,
})
}
// GetStats returns training dashboard statistics
// GET /sdk/v1/training/stats
func (h *TrainingHandlers) GetStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
stats, err := h.store.GetTrainingStats(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// VerifyCertificate verifies a certificate
// GET /sdk/v1/training/certificates/:id/verify
func (h *TrainingHandlers) VerifyCertificate(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"})
return
}
valid, assignment, err := training.VerifyCertificate(c.Request.Context(), h.store, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"valid": valid,
"assignment": assignment,
})
}
// ============================================================================
// Certificate Endpoints
// ============================================================================
// GenerateCertificate generates a certificate for a completed assignment
// POST /sdk/v1/training/certificates/generate/:assignmentId
func (h *TrainingHandlers) GenerateCertificate(c *gin.Context) {
assignmentID, err := uuid.Parse(c.Param("assignmentId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
return
}
tenantID := rbac.GetTenantID(c)
assignment, err := h.store.GetAssignment(c.Request.Context(), assignmentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assignment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"})
return
}
if assignment.Status != training.AssignmentStatusCompleted {
c.JSON(http.StatusBadRequest, gin.H{"error": "assignment is not completed"})
return
}
if assignment.QuizPassed == nil || !*assignment.QuizPassed {
c.JSON(http.StatusBadRequest, gin.H{"error": "quiz has not been passed"})
return
}
// Generate certificate ID
certID := uuid.New()
if err := h.store.SetCertificateID(c.Request.Context(), assignmentID, certID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Audit log
userID := rbac.GetUserID(c)
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
TenantID: tenantID,
UserID: &userID,
Action: training.AuditActionCertificateIssued,
EntityType: training.AuditEntityCertificate,
EntityID: &certID,
Details: map[string]interface{}{
"assignment_id": assignmentID.String(),
"user_name": assignment.UserName,
"module_title": assignment.ModuleTitle,
},
})
// Reload assignment with certificate_id
assignment, _ = h.store.GetAssignment(c.Request.Context(), assignmentID)
c.JSON(http.StatusOK, gin.H{
"certificate_id": certID,
"assignment": assignment,
})
}
// DownloadCertificatePDF generates and returns a PDF certificate
// GET /sdk/v1/training/certificates/:id/pdf
func (h *TrainingHandlers) DownloadCertificatePDF(c *gin.Context) {
certID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"})
return
}
assignment, err := h.store.GetAssignmentByCertificateID(c.Request.Context(), certID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assignment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
return
}
// Get module for title
module, _ := h.store.GetModule(c.Request.Context(), assignment.ModuleID)
courseName := assignment.ModuleTitle
if module != nil {
courseName = module.Title
}
score := 0
if assignment.QuizScore != nil {
score = int(*assignment.QuizScore)
}
issuedAt := assignment.UpdatedAt
if assignment.CompletedAt != nil {
issuedAt = *assignment.CompletedAt
}
// Use academy PDF generator
pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{
CertificateID: certID.String(),
UserName: assignment.UserName,
CourseName: courseName,
Score: score,
IssuedAt: issuedAt,
ValidUntil: issuedAt.AddDate(1, 0, 0),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "PDF generation failed: " + err.Error()})
return
}
c.Header("Content-Disposition", "attachment; filename=zertifikat-"+certID.String()[:8]+".pdf")
c.Data(http.StatusOK, "application/pdf", pdfBytes)
}
// ListCertificates returns all certificates for a tenant
// GET /sdk/v1/training/certificates
func (h *TrainingHandlers) ListCertificates(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
certificates, err := h.store.ListCertificates(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"certificates": certificates,
"total": len(certificates),
})
}